Implementing Synchronous Unary RPCs with gRPC in Java
Learn how to implement synchronous Unary RPCs in Java using gRPC and Protocol Buffers to ensure type-safe, high-performance communication between microservices.
03 Apr 2026, 00:30 UTC

The Challenge: Type-Safe Synchronous Communication
When building microservices, relying on REST/JSON often leads to runtime errors due to missing fields or incorrect types. gRPC solves this by using Protocol Buffers (protobuf) to enforce a strict contract between the client and server. For many internal service-to-service calls, a Unary RPC—a simple request-response pattern—is the most efficient way to ensure both sides agree on the data structure before a single line of logic is executed.
Prerequisites
- Java Development Kit (JDK) 11 or higher.
- Maven or Gradle build tool.
- The
protoccompiler installed on your system. - gRPC Java libraries (
grpc-netty-shaded,grpc-protobuf, andgrpc-stub).
Step 1: Defining the Service Contract
The .proto file is the single source of truth. It defines the messages (data) and the service (methods). Create a file named greeter.proto:
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.example.grpc";
// The request message containing the user's name
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
// The greeting service definition
service Greeter {
// A Unary RPC: one request, one response
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
Step 2: Generating the Java Stubs
Run the protoc compiler with the gRPC Java plugin. If using Maven, the protobuf-maven-plugin handles this during the generate-sources phase. This process creates two critical components:
- Base Classes: An abstract class (e.g.,
GreeterImplBase) that the server must extend. - Stubs: Client-side proxies used to invoke the remote methods.
Step 3: Implementing the Server Logic
To handle the request, you must override the generated method in the base class. Use the StreamObserver to send the response and signal completion.
public class GreeterService extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
// Business logic: construct the response
String greeting = "Hello, " + request.getName();
HelloReply reply = HelloReply.newBuilder().setMessage(greeting).build();
// Send the response to the client
responseObserver.onNext(reply);
// Mark the RPC as complete
responseObserver.onCompleted();
}
}
To start the server, bind the service to a Netty-based server instance:
Server server = ServerBuilder.forPort(50051)
.addService(new GreeterService())
.build();
server.start();
// Keep the main thread alive
server.awaitTermination();
Step 4: Creating the Synchronous Client
The client requires a ManagedChannel to manage the connection. For synchronous calls, use a blocking stub. This makes the gRPC call behave like a local method call, pausing execution until the server responds.
// Create a channel to the server host and port
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext() // Disable TLS for local development
.build();
// Create a blocking stub
GreeterGrpc.GreeterBlockingStub blockingStub = GreeterGrpc.newBlockingStub(channel);
// Construct the request
HelloRequest request = HelloRequest.newBuilder().setName("ReadMeFeed User").build();
// Execute the synchronous call
HelloReply response = blockingStub.sayHello(request);
System.out.println("Response: " + response.getMessage());
channel.shutdown();
Comparison: Blocking vs. Async Stubs
| Feature | Blocking Stub | Async Stub |
|---|---|---|
| Execution | Waits for server response | Returns immediately |
| Complexity | Low (Linear flow) | High (Callback-based) |
| Use Case | Internal tools, simple APIs | High-throughput, event-driven systems |
Verification and Diagnostics
To verify the implementation without a client, use grpcurl (a command-line tool for gRPC). Run this from your terminal:
# Replace localhost:50051 with your server address
# Note: Server must have reflection enabled for this to work
grpcurl -plaintext -d '{"name": "Test"}' localhost:50051 com.example.grpc.Greeter/SayHello
Expected Result: A JSON object {"message": "Hello, Test"}. If the server is unreachable, you will receive a UNAVAILABLE status code.
Critical Limitations
- HTTP/2 Requirement: gRPC cannot run over HTTP/1.1. If you are placing this behind a load balancer, ensure the balancer supports HTTP/2 and gRPC.
- Message Size: By default, gRPC limits messages to 4MB. If you attempt to send larger payloads, the server will return a
RESOURCE_EXHAUSTEDerror. You must adjustmaxInboundMessageSizein theServerBuilder. - Proto Versioning: If the client uses a different
.protoversion than the server, serialization may fail or fields may be ignored. Always distribute the.protofile as a shared library.
Rollback and Cleanup
Since this implementation changes the network state of the host, ensure the server is shut down gracefully to release the TCP port:
server.shutdown();
// Wait for existing RPCs to finish
if (!server.awaitTermination(30, TimeUnit.SECONDS)) {
server.shutdownNow();
}0 replies
A thoughtful contribution can make all the difference. Be the first to share one.