Protobuf isn’t just about making your RPCs smaller; it’s a fundamental shift in how you define and evolve your data contracts.

Let’s see this in action. Imagine we’re building a simple user service. We’ll define our User message and a UserService with a GetUser RPC.

// user.proto
syntax = "proto3";

package com.example.userservice;

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

service UserService {
  rpc GetUser (User) returns (User);
}

Now, we use the protoc compiler with the grpc-java plugin to generate Java code.

protoc \
  --plugin=protoc-gen-grpc-java=/path/to/your/grpc-java-compiler/bin/protoc-gen-grpc-java \
  --java_out=src/main/java \
  --grpc-java_out=src/main/java \
  user.proto

This generates two main classes: UserProto.java (containing the User message builder and object) and UserServiceGrpc.java (containing UserServiceGrpc.java for the server and UserServiceStub.java for the client).

On the server side, we extend UserServiceImplBase:

// UserServiceServer.java
public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {
    @Override
    public void getUser(UserProto.User request, StreamObserver<UserProto.User> responseObserver) {
        // Simulate fetching user data
        UserProto.User user = UserProto.User.newBuilder()
            .setUserId(request.getUserId())
            .setName("Jane Doe")
            .setEmail("jane.doe@example.com")
            .build();
        responseObserver.onNext(user);
        responseObserver.onCompleted();
    }
}

And on the client side, we use a generated stub:

// UserServiceConsumer.java
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051).usePlaintext().build();
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);

UserProto.User userRequest = UserProto.User.newBuilder().setUserId("123").build();
UserProto.User userResponse = stub.getUser(userRequest);

System.out.println("User: " + userResponse.getName());
channel.shutdown();

The magic of Protobuf lies in its schema evolution. You can add new fields to your User message without breaking existing clients, as long as you don’t change the field numbers. Old clients will simply ignore the new fields. This makes deploying and updating services a much smoother process.

The protoc compiler is a powerful tool, but understanding its plugins is key. The grpc-java plugin specifically generates the gRPC service interfaces and stubs that make communication between services seamless. It handles the serialization and deserialization of your Protobuf messages, allowing you to focus on your business logic.

One of the most understated aspects of gRPC and Protobuf is how they enforce a clear, language-agnostic contract. When you define your .proto files, you’re not just writing code; you’re creating a shared understanding of data structures and service APIs that any language with a Protobuf implementation can consume. This drastically reduces integration headaches, especially in polyglot environments.

The core of gRPC’s efficiency comes from Protobuf’s binary serialization format. Unlike JSON or XML, Protobuf serializes data into a compact binary representation. This means smaller payloads, faster network transmission, and less CPU overhead for parsing, which is critical for high-performance microservices.

The next logical step is exploring streaming RPCs, where clients or servers can send sequences of messages.

Want structured learning?

Take the full Grpc course →