Of course! Here’s a comprehensive guide to implementing RPC (Remote Procedure Call) in Java using Protocol Buffers (Protobuf), covering everything from the basics to a complete, runnable example.

We will use gRPC, which is the modern, high-performance RPC framework developed by Google that uses Protobuf as its Interface Definition Language (IDL).
Why Use gRPC with Protobuf?
- Performance: Protobuf serializes data into a compact binary format, which is much smaller and faster to process than text-based formats like JSON or XML.
- Strongly-Typed API: The
.protofile acts as a strict contract between the client and server. Both sides are generated from the same file, eliminating mismatched API issues. - Code Generation: Protobuf compilers generate type-safe client and server code in multiple languages (Java, Python, Go, C++, etc.), saving you from writing and maintaining boilerplate network code.
- Multi-lingual: The same service definition can be used to generate clients and servers in different languages.
- Rich Ecosystem: gRPC features pluggable support for load balancing, tracing, authentication, and more.
The Core Components of a gRPC Application
- Service Definition: A
.protofile that defines the service, its methods, and the request/response message types. - Generated Code: Java classes created from the
.protofile. This includes:- Base classes for your service (e.g.,
GreeterGrpc.GreeterImplBase). - Model classes for all your messages (e.g.,
HelloRequest,HelloReply).
- Base classes for your service (e.g.,
- Server Implementation: A Java class that extends the generated service base class and implements the business logic for each RPC method.
- Client Implementation: A Java client that uses the generated stub to make calls to the server.
Step-by-Step Example: A Simple Greeter Service
Let's build a simple "Greeter" service that takes a HelloRequest and returns a HelloReply.
Prerequisites
- Java 8 or newer
- Maven (or Gradle, but we'll use Maven here)
- Protocol Buffers Compiler (
protoc) - Download instructions
Step 1: Set Up Your Maven Project
Create a new Maven project with the following pom.xml. This file includes the necessary dependencies for grpc, protobuf, and a Maven plugin to automatically compile .proto files.
<!-- pom.xml -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>grpc-java-example</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<grpc.version>1.58.0</grpc.version>
<protobuf.version>3.24.4</protobuf.version>
</properties>
<dependencies>
<!-- gRPC dependencies -->
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<!-- Needed for Java 9+ -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.53</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.0</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Step 2: Define the Service in a .proto File
Create the source directory src/main/proto and add a file named greeter.proto.

// src/main/proto/greeter.proto
syntax = "proto3";
// The package name should match your Maven groupId
package com.example;
// Define the service
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
Step 3: Generate the Java Code
Now, run the Maven compile command. The protobuf-maven-plugin will automatically execute protoc to generate the Java files.
mvn clean compile
You will find the generated Java files in target/generated-sources/protobuf/java/ and target/generated-sources/protobuf/grpc/. Your IDE (like IntelliJ or Eclipse) should automatically pick up these source directories.
- Messages:
com.example.HelloRequest.java,com.example.HelloReply.java - Service Base:
com.example.GreeterGrpc.java(contains the base classGreeterImplBaseand the client stubGreeterGrpc.GreeterBlockingStub)
Step 4: Implement the Server
Create a new Java class for your server implementation. This class will extend the generated GreeterImplBase and override the sayHello method.
// src/main/java/com/example/GrpcServer.java
package com.example;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
public class GrpcServer {
private Server server;
private void start() throws IOException {
/* The port on which the server should run */
int port = 50051;
server = ServerBuilder.forPort(port)
.addService(new GreeterServiceImpl())
.build()
.start();
System.out.println("Server started, listening on " + port);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
// Use stderr here since the logger may have been reset by its JVM shutdown hook.
System.err.println("*** shutting down gRPC server since JVM is shutting down");
GrpcServer.this.stop();
System.err.println("*** server shut down");
}));
}
private void stop() {
if (server != null) {
server.shutdown();
}
}
/**
* Await termination on the main thread since the grpc library uses daemon threads.
*/
private void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
/**
* Main launches the server from the command line.
*/
public static void main(String[] args) throws IOException, InterruptedException {
final GrpcServer server = new GrpcServer();
server.start();
server.blockUntilShutdown();
}
// Implement the service
static class GreeterServiceImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
System.println("Received request from: " + request.getName());
String message = "Hello " + request.getName();
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
}
Step 5: Implement the Client
Create a client that connects to the server and calls the SayHello method.

// src/main/java/com/example/GrpcClient.java
package com.example;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
public class GrpcClient {
public static void main(String[] args) {
String target = "localhost:50051";
// Create a channel to the server
ManagedChannel channel = ManagedChannelBuilder.forTarget(target)
// Channels are secure by default (via SSL/TLS). Disable this for local dev.
.usePlaintext()
.build();
try {
// Create a blocking (synchronous) stub
GreeterGrpc.GreeterBlockingStub blockingStub = GreeterGrpc.newBlockingStub(channel);
// Prepare the request
HelloRequest request = HelloRequest.newBuilder().setName("gRPC Java Client").build();
// Call the RPC method and get the response
HelloReply response = blockingStub.sayHello(request);
// Print the response
System.out.println("Response from server: " + response.getMessage());
} finally {
// ManagedChannel should be shutdown on application exit
channel.shutdownNow();
}
}
}
Step 6: Run the Application
-
Run the Server: Execute the
mainmethod inGrpcServer.java. You should see the output:Server started, listening on 50051 -
Run the Client: Execute the
mainmethod inGrpcClient.java. You should see the output:Response from server: Hello gRPC Java Client -
Check the Server Console: You should also see this new output in the server's console:
Received request from: gRPC Java Client
Congratulations! You have successfully implemented a gRPC client-server application in Java using Protocol Buffers.
Advanced gRPC Concepts
The example above uses unary RPCs (one request, one response). gRPC supports more complex interaction patterns defined in your .proto file:
Server Streaming RPC
The server sends back a stream of messages after receiving a client's single request.
In greeter.proto:
rpc LotsOfReplies (HelloRequest) returns (stream HelloReply) {}
Server Implementation:
@Override
public void lotsOfReplies(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
for (int i = 0; i < 5; i++) {
String message = "Hello " + name + " - " + i;
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
}
responseObserver.onCompleted();
}
Client Streaming RPC
The client sends a stream of messages to the server, and the server responds with a single message.
In greeter.proto:
rpc LotsOfGreetings (stream HelloRequest) returns (HelloReply) {}
Server Implementation:
@Override
public StreamObserver<HelloRequest> lotsOfGreetings(StreamObserver<HelloReply> responseObserver) {
return new StreamObserver<HelloRequest>() {
StringBuilder namesBuilder = new StringBuilder();
@Override
public void onNext(HelloRequest value) {
namesBuilder.append(value.getName()).append(", ");
System.out.println("Got name: " + value.getName());
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onCompleted() {
String message = "Hello " + namesBuilder.substring(0, namesBuilder.length() - 2);
HelloReply reply = HelloReply.newBuilder().setMessage(message).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
};
}
Bidirectional Streaming RPC
Both the client and server can send messages to each other independently. The order of messages is preserved within each stream.
In greeter.proto:
rpc BidiHello (stream HelloRequest) returns (stream HelloReply) {}
Server Implementation:
@Override
public StreamObserver<HelloRequest> bidiHello(StreamObserver<HelloReply> responseObserver) {
return new StreamObserver<HelloRequest>() {
@Override
public void onNext(HelloRequest value) {
String message = "Hello " + value.getName();
responseObserver.onNext(HelloReply.newBuilder().setMessage(message).build());
}
@Override
public void onError(Throwable t) {
t.printStackTrace();
}
@Override
public void onCompleted() {
responseObserver.onCompleted();
}
};
} 