Microservices + gRPC: Building High-Performance Service-to-Service Communication
1. Why Do Microservices Need Internal Communication?
Breaking an application into microservices gives us services with separate responsibilities.
For example:
API Gateway
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
Auth Product Payment
Service Service Service
Each service owns a specific part of the system:
Auth Service
└── Users and authentication
Product Service
└── Products and inventory
Payment Service
└── Payments and transactions
But services rarely operate independently.
A request may require:
API Gateway
│
▼
Product Service
│
▼
Inventory Service
A checkout flow may involve several services:
Order Service
│
├──► Product Service
└──► Payment Service
This creates a service-to-service communication problem.
HTTP with JSON works well:
Service A
│
│ HTTP + JSON
▼
Service B
But as internal communication grows, we may want stronger contracts, generated clients, efficient serialization, and streaming.
This is where gRPC becomes useful.
2. REST Is Not Always the Best Internal Protocol
REST is a strong choice for browser-facing APIs, public APIs, and third-party integrations.
A typical request might be:
GET /products/123
with:
{
"id": "123",
"name": "Keyboard",
"price": 4999
}
The model is straightforward:
Client
│
│ HTTP
▼
Server
│
│ JSON
▼
Response
Inside a larger microservices system, however, one service may communicate with many others:
Order Service
│
├── GET /products/:id
├── GET /users/:id
├── POST /payments
└── GET /inventory/:id
Now every service needs to agree on:
Routes
HTTP Methods
Request Structures
Response Structures
Validation
Error Formats
Those contracts may live in OpenAPI definitions, shared types, or documentation.
gRPC starts from a different model:
Service Contract
│
▼
Protocol Buffer
│
┌──┴──┐
▼ ▼
Client Server
Instead of primarily defining URLs, we define the service interface first.
3. Introducing gRPC
gRPC is a Remote Procedure Call framework designed for distributed service communication.
The basic idea is:
Client
│
│ call method
▼
Server
│
│ execute method
▼
Response
From the client's perspective, a remote operation can look similar to a method call.
Instead of:
fetch("/products/123");
a generated client can expose:
productClient.getProduct({
id: "123",
});
The important part is that both sides work from the same service definition.
By default, gRPC uses Protocol Buffers as its interface definition language and message format, with tooling that can generate client and server code for supported languages.
This gives us a contract-first communication model.
4. Define the Contract With Protocol Buffers
The service can be defined in a .proto file:
syntax = "proto3";
service ProductService {
rpc GetProduct(GetProductRequest)
returns (ProductResponse);
}
message GetProductRequest {
string id = 1;
}
message ProductResponse {
string id = 1;
string name = 2;
double price = 3;
}
This describes:
ProductService
│
└── GetProduct()
│
├── Request
└── Response
Both the client and server use the same contract:
product.proto
│
┌───────┴───────┐
▼ ▼
gRPC Client gRPC Server
│ │
└───────┬───────┘
▼
Same Contract
The Protocol Buffer compiler and language-specific plugins can generate the communication code from this definition.
That removes much of the manually maintained client/server contract.
5. Building and Calling the Service
Suppose the Product Service is written in Node.js.
Its implementation can expose the RPC defined by the contract:
const productService = {
async getProduct(call, callback) {
const product = await findProduct(
call.request.id
);
callback(null, {
id: product.id,
name: product.name,
price: product.price,
});
},
};
The server registers the implementation:
server.addService(
ProductService.service,
productService
);
and exposes gRPC on its internal port:
Product Service
│
└── gRPC :50051
Another service can then call it through the generated client:
const response =
await productClient.getProduct({
id: "123",
});
The architecture becomes:
Order Service
│
│ GetProduct()
▼
Product Service
│
▼
ProductResponse
The client does not need to manually construct URLs, HTTP methods, JSON payloads, or response parsing logic.
The service contract defines the operation.
6. Why Protocol Buffers Matter
The value of gRPC is not only the RPC abstraction.
Protocol Buffers provide a shared schema.
Instead of loosely structured JSON:
{
"id": "123",
"name": "Keyboard",
"price": 4999
}
the structure is explicitly defined:
message ProductResponse {
string id = 1;
string name = 2;
double price = 3;
}
This contract is independent of the implementation language.
For example:
Auth Service → Go
Product Service → Node.js
Payment Service → Java
Recommendation → Python
Each service can use a different language while communicating through the same definitions.
That becomes especially useful in larger polyglot systems.
7. Unary RPC and Streaming
The simplest gRPC operation is unary RPC:
Client
│
│ Request
▼
Server
│
│ Response
▼
Client
For example:
rpc GetProduct(
GetProductRequest
) returns (
ProductResponse
);
But gRPC also supports streaming.
Server Streaming
One request can produce multiple responses:
Client
│
│ Request
▼
Server
│
├── Response
├── Response
├── Response
└── Response
rpc WatchInventory(
InventoryRequest
) returns (
stream InventoryUpdate
);
Client Streaming
The client can send multiple messages before receiving a final response:
Client
│
├── Request
├── Request
└── Request
│
▼
Server
Bidirectional Streaming
Both sides can continuously exchange messages:
Client
│
├────────►
◄────────┤
├────────►
◄────────┤
rpc Chat(
stream ClientMessage
) returns (
stream ServerMessage
);
So gRPC supports:
Unary
Server Streaming
Client Streaming
Bidirectional Streaming
This is one of the major differences between a basic RPC interface and a traditional request/response API.
8. REST at the Edge, gRPC Inside
A microservices architecture does not have to choose one protocol for everything.
A common pattern is:
Internet
│
│ HTTP / JSON
▼
API Gateway
│
┌──────────┼──────────┐
│ │ │
gRPC gRPC gRPC
│ │ │
▼ ▼ ▼
Product Auth Payment
Service Service Service
The external boundary can remain REST/HTTP:
Browser
│
│ REST
▼
API Gateway
while internal communication uses gRPC:
API Gateway
│
│ gRPC
▼
Microservices
This gives each boundary an appropriate interface.
External APIs may benefit from:
HTTP
JSON
REST
OpenAPI
while internal services may benefit from:
gRPC
Protocol Buffers
Generated Clients
Streaming
The useful question is therefore not:
REST or gRPC?
It is:
Where should each communication model be used?
9. Deadlines, Errors and Metadata
Microservices introduce network failures that do not exist in a local function call.
For example:
Order Service
│
▼
Payment Service
│
X
Timeout
A service should not wait indefinitely.
gRPC supports deadlines:
const deadline =
new Date(Date.now() + 3000);
client.getPayment(
request,
{ deadline },
callback
);
Conceptually:
Request
│
▼
Payment Service
│
│ 3 seconds
▼
Deadline
If the deadline is exceeded, the caller can receive a DEADLINE_EXCEEDED status.
gRPC also provides standard status codes such as:
OK
INVALID_ARGUMENT
NOT_FOUND
UNAUTHENTICATED
PERMISSION_DENIED
DEADLINE_EXCEEDED
UNAVAILABLE
INTERNAL
Service communication may also need metadata such as:
Authorization
Request ID
Trace ID
Tenant ID
Correlation ID
For example:
const metadata = new Metadata();
metadata.set(
"authorization",
`Bearer ${token}`
);
metadata.set(
"x-request-id",
requestId
);
This becomes important when a request crosses several services and needs to be traced through the system.
10. gRPC vs REST
The difference can be summarized as:
RESTgRPCInterfaceHTTP endpointsRPC methodsPayloadUsually JSONProtocol BuffersContractOpenAPI / conventions.protoClient generationOptionalGenerated from contractStreamingPossibleBuilt into RPC modelBrowser usageDirectUsually through gRPC-Web or a gatewayPublic APIsStrong fitLess commonInternal service communicationCommonStrong fit
This does not make gRPC a universal replacement for REST.
REST remains useful for:
Public APIs
Browser-facing APIs
Third-party integrations
Simple HTTP resources
gRPC is particularly useful for:
Internal service communication
Strong contracts
Generated clients
Streaming
Polyglot services
The two can coexist.
11. Where gRPC Fits in the Architecture
A practical architecture might look like:
Clients
│
│ HTTP / JSON
▼
API Gateway
│
┌────────────┼────────────┐
│ │ │
gRPC gRPC gRPC
│ │ │
▼ ▼ ▼
Auth Product Order
Service Service Service
│
│ gRPC
▼
Payment
Service
Each service can maintain:
Own Database
Own Business Logic
Own Deployment
Own gRPC Interface
The contracts can also live alongside the services:
monorepo/
│
├── backend-services/
│ ├── auth-service/
│ ├── product-service/
│ ├── order-service/
│ └── payment-service/
│
└── libs/
└── proto/
├── auth.proto
├── product.proto
├── order.proto
└── payment.proto
This works well in a monorepo because the contracts and their implementations can evolve together.
12. The Bigger Picture
The real value of gRPC is not simply sending data faster than JSON.
The larger architectural change is moving from loosely coordinated HTTP endpoints toward explicit service contracts.
Without a formal contract:
Service A
│
│ HTTP + JSON
▼
Service B
Documentation
+
Conventions
+
Manual Validation
With gRPC:
service.proto
│
┌───────┴───────┐
▼ ▼
Client Server
│ │
└───── gRPC ────┘
The contract becomes part of the architecture.
That matters more as the number of services grows.
The architectural principle is:
Microservices are not simply about splitting an application into smaller services. They are about defining clear boundaries and reliable contracts between those services.
gRPC provides one way to make those contracts explicit, strongly typed, and executable.
13. When Should You Use gRPC?
gRPC is a strong fit when the system needs:
Service-to-Service Communication
+
Strong Contracts
+
Generated Clients
+
Streaming
+
Polyglot Services
It may be unnecessary for a simple system:
Frontend
│
▼
Backend
with only a few straightforward HTTP endpoints.
The goal should not be to introduce gRPC because it is technically interesting.
Use it where the communication boundary benefits from the model it provides.
A practical architecture often ends up as:
External Boundary
│
▼
REST / HTTP
│
▼
API Gateway
│
▼
Internal Boundary
│
▼
gRPC
│
┌───┼───┐
▼ ▼ ▼
Auth Product Payment
REST and gRPC are therefore not competing choices for the entire system.
They can be complementary parts of the same architecture.