In earlier steps of a typical gRPC project, you usually focus on the server side first. You define your .proto file, generate source code using the Protocol Buffer compiler, implement the service, and start a gRPC server that exposes your API. Once that is done, you might test the API using tools or simple test setups and confirm that everything works.
However, in real-world systems, APIs are not consumed manually—they are consumed by other applications. This is where the gRPC client comes into the picture. A gRPC service is only truly useful when another application can call it programmatically.
This tutorial explains how a gRPC client invokes a remote API and what happens behind the scenes.
1. Recap: What We Already Have
Before building a client, let us quickly summarize what already exists on the server side.
We already:
- Defined a
.protofile with an RPC method. - Used the Protocol Buffer compiler to generate Java code.
- Received a generated abstract service base class.
- Implemented the service logic.
- Registered the service with a gRPC server.
- Started the server to expose the API.
At this stage, the server is running and ready to accept requests. The next logical question is: How does another application call this API?
2. Real-World Perspective
In production systems:
- One service acts as a provider (server).
- Another service acts as a consumer (client).
- The consumer invokes APIs exposed by the provider.
For example:
- A Product Service might call a Bank Service.
- A User Service might call an Account Service.
- Microservices frequently communicate this way.
gRPC supports this pattern seamlessly by generating client code from the same .proto file.
3. The Role of Generated Client Code
When you compile a .proto file, gRPC generates:
- Server-side base classes
- Message classes (request/response)
- Client-side stubs
The stub is the key client-side component. A stub behaves like a local object but actually communicates with a remote service. You invoke methods on the stub as if calling a normal Java method. Internally, gRPC handles:
- Serialization
- Network transmission
- Deserialization
- Response handling
4. Understanding the Channel
Before using a stub, a client must create a channel. A channel represents the connection between the client and server.
Think of it as: A managed communication pipe that knows how to reach the server.
Just as the server uses a ServerBuilder, the client uses a ManagedChannelBuilder.
5. Creating a gRPC Client
Let us walk through a simple client example.
Step 1: Create the Channel
ManagedChannel channel =
ManagedChannelBuilder
.forAddress("localhost", 6565)
.usePlaintext()
.build();
Explanation
forAddress(host, port)tells the client where the server is.usePlaintext()disables TLS.
By default, gRPC assumes secure communication, so for local demos you must explicitly allow plaintext.build()creates the channel.
Without the correct host and port, the client cannot reach the server.
Step 2: Create the Stub
The stub is created using the generated gRPC class.
BankServiceGrpc.BankServiceBlockingStub stub =
BankServiceGrpc.newBlockingStub(channel);
Explanation
- The stub is generated from the
.protofile. - It represents the remote service.
- The blocking stub performs synchronous calls.
You may also see:
- Async stubs
- Future stubs
For learning, the blocking stub is simplest.
Step 3: Build the Request
BalanceCheckRequest request =
BalanceCheckRequest.newBuilder()
.setAccountNumber(2)
.build();
Protocol Buffers enforce type safety and builder patterns.
This ensures:
- Strong typing
- Validation at compile time
- Structured message formats
Step 4: Call the Remote Method
Balance response =
stub.getAccountBalance(request);
This looks like a normal Java method call, but it is not.
Behind the scenes:
- The request is serialized.
- It is sent over the channel.
- The server processes it.
- The response is serialized back.
- The client receives and deserializes it.
All of this happens automatically.
Step 5: Use the Response
System.out.println(response.getBalance());
At this point, the client has received real data from a remote service.
6. Full Example Client Program
public class GrpcClient {
public static void main(String[] args) {
ManagedChannel channel =
ManagedChannelBuilder
.forAddress("localhost", 6565)
.usePlaintext()
.build();
BankServiceGrpc.BankServiceBlockingStub stub =
BankServiceGrpc.newBlockingStub(channel);
BalanceCheckRequest request =
BalanceCheckRequest.newBuilder()
.setAccountNumber(2)
.build();
Balance response =
stub.getAccountBalance(request);
System.out.println("Balance: " + response.getBalance());
channel.shutdown();
}
}
7. Important Observations
The Client and Server Are Separate Applications
Even if both exist in the same project:
- They run in different JVMs.
- They behave like independent applications.
- Communication happens over the network.
The Stub Makes Remote Calls Feel Local
You simply call:
stub.getAccountBalance(...)
It feels like calling:
String.toUpperCase()
But in reality, it is a remote procedure call.
