Protobuf and gRPC are a powerful combination for building efficient, language-agnostic microservices, but understanding how they fit together can be a bit of a leap.

Let’s see it in action. Imagine we’re building a simple user service.

First, we define our data structures and the methods our service will expose using Protocol Buffers. This .proto file is the lingua franca.

// user.proto
syntax = "proto3";

package userservice;

message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
}

message GetUserRequest {
  int64 user_id = 1;
}

message GetUserResponse {
  User user = 1;
}

service UserService {
  rpc GetUser(GetUserRequest) returns (GetUserResponse);
}

Here, User is a message type representing our data. GetUserRequest and GetUserResponse define the input and output for our GetUser method. The service UserService block declares the actual remote procedure call (RPC) that our server will implement and clients will call.

Now, the magic happens with code generation. We use the Protobuf compiler (protoc) along with the appropriate gRPC plugin for our language. For Go, it looks like this:

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

This command takes user.proto and generates two sets of Go files:

  1. user.pb.go: Contains the Go structs for User, GetUserRequest, and GetUserResponse, along with serialization/deserialization logic.
  2. user_grpc.pb.go: Contains the Go interfaces for our UserService server and client stubs.

The generated user.pb.go file will have Go structs like this:

// user.pb.go (simplified)
type User struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	Id    int64  `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
	Name  string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
	Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
}

// ... other generated structs

And the user_grpc.pb.go file will define the server interface and client stub:

// user_grpc.pb.go (simplified)
type UserServiceServer interface {
	GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
	mustEmbedUnimplementedUserServiceServer()
}

type userServiceClient struct {
	cc grpc.ClientConnInterface
}

func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient {
	return &userServiceClient{cc}
}

func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest) (*GetUserResponse, error) {
	// ... gRPC client logic
}

This generated code provides the concrete data types and the abstract interfaces needed to build both the gRPC server and client. On the server side, you’ll create a struct that implements the UserServiceServer interface. On the client side, you’ll use the NewUserServiceClient function to create a stub that lets you call the remote GetUser method as if it were a local function.

The core problem Protobuf and gRPC solve is the impedance mismatch between different programming languages and the need for efficient, type-safe communication over the network. Instead of manually serializing and deserializing JSON or XML, you define your data and service contracts once in .proto files, and the code generation tools handle the rest. This drastically reduces boilerplate, eliminates common serialization errors, and ensures that both client and server agree on the exact structure of the data being exchanged.

The most surprising part of this system is how the generated code for the client stub implicitly handles all the network plumbing. When you call client.GetUser(ctx, request), the generated client code serializes the request message into Protobuf binary format, constructs an HTTP/2 frame, sends it over the network to the server, waits for the server’s response, deserializes the Protobuf binary response, and then returns the GetUserResponse struct and any error. You’re interacting with a Go function, but under the hood, a complex network interaction is occurring seamlessly.

The next step is to implement the server logic and connect it to a gRPC server instance.

Want structured learning?

Take the full Protobuf course →