Code Generation and Stubs

The magic that makes an RPC feel like a local function call is code generation. You run a compiler over your `.proto` file and out come typed classes and methods for your language — the client stub you call and the server interface you implement. Understanding what's generated, and the boundary between generated and hand-written code, is what turns gRPC from a black box into a tool you control.

We’ve written a .proto contract and chosen call types. Now: how does that text become callable code? The answer is the protobuf compiler, protoc, plus a gRPC plugin for your language. This post is about the generation step — what goes in, what comes out, and how you build on it without fighting it.

From .proto to code

The compiler protoc reads your .proto file and, with the appropriate language plugin, emits source code. For a message like User, it generates a class or struct with typed fields, accessors, and serialization built in. For a service, it generates two things that mirror each other across the network:

A typical Go generation command wires the two plugins together:

protoc --go_out=. --go-grpc_out=. \
       --go_opt=paths=source_relative \
       --go-grpc_opt=paths=source_relative \
       user/v1/user.proto

This produces user.pb.go (the message types and their serialization) and user_grpc.pb.go (the client stub and server interface). Every language has its equivalent plugin; the output shape is the same everywhere because it all derives from the one contract.

The two sides of the generated boundary

The generated code defines a precise seam between “framework’s job” and “your job.” Seeing it clearly is what makes gRPC feel simple.

On the server side, you implement the generated interface — writing only the business logic:

type userServer struct {
    pb.UnimplementedUserServiceServer   // forward-compatibility embed
}

func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    u, err := s.db.FindUser(ctx, req.GetId())
    if err != nil {
        return nil, status.Errorf(codes.NotFound, "user %d not found", req.GetId())
    }
    return &pb.User{Id: u.ID, Name: u.Name, Email: u.Email}, nil
}

You receive an already-deserialized, typed *GetUserRequest and return a typed *User (or an error). Everything around it — the network, the bytes, the framing — is the framework’s concern. Note the embedded UnimplementedUserServiceServer: that’s a forward-compatibility device so that when you regenerate after adding a new method to the .proto, your existing server still compiles (the new method has a default “unimplemented” behavior until you write it).

On the client side, you connect and call:

conn, _ := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
client := pb.NewUserServiceClient(conn)

user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 42})

client.GetUser looks like an ordinary method. Behind it, the stub serializes GetUserRequest, sends it over the connection, waits for the response, and hands you a typed *User. The RPC-as-local-call promise from post 1 is delivered right here, by generated code.

Why generated code is the point

It’s tempting to see codegen as boilerplate you tolerate. It’s actually the source of gRPC’s core guarantees:

The generated code is not overhead around the real work — it is the mechanism that makes cross-service, cross-language calls safe.

Working with codegen in practice

Because generated code is derived from the .proto, a few habits keep it healthy:

Key takeaways

Further reading

Sources & References

Stubs, generated code, and the service interface