What Is gRPC?
gRPC is an open-source RPC (Remote Procedure Call) framework from Google. The core idea: calling a method on another service should feel like calling a local method — the network transport, serialization, and deserialization are all handled by the framework for you.
Traditionally, sending data between services looks roughly like this:
// curl version
curl -X POST localhost:8080/api/orders \
-H 'Content-Type: application/json' \
-d '{"userId":"u123","amount":3000}'
// java code
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://httpbin.org/get"))
.GET()
.build();
HttpResponse<String> response = client.send(
request,
HttpResponse.BodyHandlers.ofString()
);
This is the approach we’re all familiar with — before sending a request, you need to know the target’s domain, the path, the HTTP method, plus whatever headers and request body the spec requires.
gRPC doesn’t work that way.
Before you can use gRPC, you first write a .proto file that defines the interface — a contract between the caller and the callee, also known as an Interface Definition Language (IDL). It looks like this:
syntax = "proto3";
service WalletService {
rpc GetBalance (BalanceRequest) returns (BalanceResponse);
}
message BalanceRequest {
string user_id = 1;
}
message BalanceResponse {
int64 balance = 1;
string currency = 2;
}
If you think about it in Java backend terms:
syntax— specifies the language/spec versionservice— how the two sides talk to each other; it reads like a local class, but it’s actually sitting on a different server. Think of it as aninterfacefor a communication framework.rpc— a single gRPC service can contain multiple methods, just like methods defined on an interfacemessage— this is your DTO / POJO, the shape of the request and response objectsfield = 1— this is the field number; only the number gets sent over the wire, not the field name
Once you’re done, you use a tool like the protobuf-maven-plugin to generate the code (I’m using Java here as the example). Drop it into your pom.xml, run compile, and it automatically generates the following files:
WalletServiceGrpc.java provides the ImplBase for the server side and the Stub for the client side. BalanceRequest and BalanceResponse are just the request/response DTOs.
Next, on the server side you write a WalletServiceImpl that extends WalletServiceGrpc.WalletServiceImplBase, and put your business logic in the subclass — the transport details are entirely the framework’s problem. Here’s a sample:
public class WalletServiceImpl extends WalletServiceGrpc.WalletServiceImplBase {
@Override
public void getBalance(BalanceRequest request, StreamObserver<BalanceResponse> responseObserver) {
BalanceResponse response = BalanceResponse.newBuilder()
.setBalance(3000L)
.setCurrency("TWD")
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
Once the server-side implementation is in place, you still need to spin up a dedicated port to serve gRPC requests. Here’s an example:
int port = 9090;
Server server = ServerBuilder.forPort(port)
.addService(new WalletServiceImpl())
.build();
server.start();
System.out.println("gRPC server started, listening on port " + port);
Runtime.getRuntime().addShutdownHook(new Thread(server::shutdown));
server.awaitTermination();
And with that, the server side is done. Let’s look at the client side.
The client side doesn’t take much: open a channel, grab a stub, and you can call it like a local method.
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 9090)
.usePlaintext() // no TLS for local testing
.build();
WalletServiceGrpc.WalletServiceBlockingStub stub = WalletServiceGrpc.newBlockingStub(channel);
BalanceResponse response = stub.getBalance(
BalanceRequest.newBuilder().setUserId("u123").build());
Compared to the HTTP version we started with, there’s no path, no HTTP method, no headers, and you didn’t have to write your own DTO. We just called getBalance(), and the framework handled the rest.
Conclusion
Today, gRPC is mostly used for communication between backend microservices. It’s well suited for systems where low latency and high throughput matter, and it’s cross-language — for example, you and your Python team can share the same .proto file, cutting down on misunderstandings between the two sides.
As for where it falls short: it’s not a great fit for public-facing APIs — REST is still the mainstream choice there — and it’s also not great for calling directly from a browser. Take HTTP caching, for instance: since gRPC always uses POST, the browser’s cache is useless, which makes it more of a hassle to work with.
One more thing to watch out for: load balancing. Since gRPC runs over a long-lived HTTP/2 connection, a traditional L4 load balancer will route every request from that connection to the same backend instance. In other words, L4 LB can effectively stop balancing anything for gRPC traffic — worth paying close attention to in practice. You’ll need an L7 LB (Envoy, Istio) or client-side load balancing instead.
On top of that, gRPC also makes debugging harder, since it sends requests as binary rather than plain text — and the tooling for inspecting that binary traffic has its own learning curve.
Alright, that should give you a solid baseline understanding of gRPC. Next up, I’m moving on to actually benchmarking it — testing just how much performance this readability-for-speed trade-off actually buys you.
That’s it for this one. See you in some other blue moon.