Skip to content

03. gRPC and Protobuf — compact binary order slips for fast internal service calls

~16 min read. When services talk a lot, bytes and latency both start mattering hard.

Built on the ELI5 in 00-eli5.md. The order slip — the exact request carried to the kitchen — becomes a typed protobuf message that is smaller, stricter, and faster to move between services.


1) Protocol Buffers give you compact, typed contracts

Protocol Buffers, or protobuf, define message schemas with numbered fields. Those field numbers matter on the wire. See. The bytes do not carry long JSON key names. They carry compact binary encodings.

A tiny example:

message CreateOrderRequest {
  string customer_id = 1;
  int32 item_count = 2;
  int64 total_paise = 3;
}

That file does three useful things. It defines shape. It defines type. It defines stable field identity. From one schema, you generate code for Go, Java, Python, and more.

Why teams like protobuf inside large systems:

  • Messages are compact.
  • Types are explicit.
  • Generated clients reduce manual drift.
  • Backward compatibility can be managed deliberately.

Worked example with numbers. Suppose a JSON request body is 1,200 bytes. The equivalent protobuf message may be around 120 bytes to 200 bytes. That is not magic every time, but 5x to 10x smaller is common for chatty internal traffic. If 50,000 requests happen each second, that byte reduction becomes real network relief.

The order slip also becomes harder to misread. A field is int64 total_paise, not a free-form string someone hopes to parse later. Simple, no?

2) gRPC services turn message contracts into RPC methods

gRPC uses protobuf to define both messages and services. A service groups callable methods. Each method names request and response types.

Example:

service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
  rpc GetOrder(GetOrderRequest) returns (Order);
}

That looks close to a normal function call. The transport work is generated for you. Clients get stubs. Servers implement handlers. This is why internal platform teams like gRPC. The contract is one place, and many languages can honor it.

A request path looks like this:

┌────────────┐     ┌──────────────┐     ┌──────────────┐
│ Client stub│ ──▶ │ HTTP/2 frame │ ──▶ │ Server stub  │
└────────────┘     └──────────────┘     └──────────────┘
        │                                      │
        └────────── generated from proto ──────┘

Unary RPC is the simplest kind. One request goes in. One response comes back. That covers many internal tasks like inventory lookup, fraud score fetch, or address validation.

Worked example. A checkout service calls pricing service 6 times during one order flow. Each unary call takes 12 ms median over gRPC. If a legacy JSON HTTP setup took 20 ms, you save about 8 ms per hop. Across 6 hops, that is roughly 48 ms median improvement. Not small when your overall budget is 300 ms.

3) HTTP/2 streaming is where gRPC becomes more than fast JSON

gRPC rides on HTTP/2. That gives multiplexing, header compression, and streaming support. Streaming changes design options. See the four call styles.

Unary:

client ──request──▶ server
client ◀─response── server

Server streaming:

client ──request──▶ server
client ◀─item1───── server
client ◀─item2───── server
client ◀─item3───── server

Client streaming:

client ──chunk1──▶ server
client ──chunk2──▶ server
client ──chunk3──▶ server
client ◀─summary── server

Bidirectional streaming:

client ──eventA──▶ server
client ◀─eventB── server
client ──eventC──▶ server
client ◀─eventD── server

Use cases become clearer with examples. Server streaming fits shipment updates for one warehouse sync call. Client streaming fits uploading 500 telemetry samples from one edge device. Bidirectional streaming fits live speech, trading feeds, or multiplayer coordination.

Worked example with numbers. A warehouse service needs to push 1,000 picking updates over 10 seconds. Polling every second may create 10 separate HTTP requests. One server-streaming gRPC call can carry the whole sequence. That cuts repeated headers, connection churn, and client coordination work.

4) Backward compatibility depends on field discipline, not hope

Protobuf evolves safely only when you respect the rules. Never reuse field numbers. Do not casually switch field meaning. Reserve deleted numbers. So what to do? Treat schema review like API review.

Good changes usually include:

  • Adding a new optional field.
  • Adding a new service method.
  • Deprecating a field before removal.

Risky changes include:

  • Reusing field number 3 for a new meaning.
  • Changing string user_id into int64 user_id without migration.
  • Making existing consumers depend on field order in generated code.

Example:

message Order {
  string id = 1;
  string status = 2;
  reserved 3;
}

That reserved 3 line protects future mistakes. The wire format remembers old choices. You should too.

When does gRPC beat REST clearly? Usually when services are internal, performance sensitive, and owned by engineering teams that can share generated clients. When does REST stay better? Public APIs, browser-first use cases, and systems where curl-level debuggability matters. The order slip is brilliant inside the kitchen. It is not always the best thing to hand directly to diners.

Mini worked example. Say inventory service sends 30 stock updates for one product in 3 seconds. A server stream can carry all 30 updates on one RPC. Thirty unary calls would repeat headers and setup work each time.

Debugging tip. Keep JSON transcoding or admin endpoints around when humans need quick inspection. Pure binary everywhere can slow incident response.

One more rule. Do not make every field required by habit. Optional growth room helps future versions survive. That is especially useful in large rolling deployments. See. Strict contracts still need operational empathy. Small bytes, clear types, fewer surprises.


Where this lives in the wild

  • A Google infrastructure engineer uses protobuf and gRPC so thousands of internal services share typed, code-generated contracts.
  • An Uber platform engineer can stream dispatch and pricing updates between internal services without bloated JSON payloads.
  • A Coinbase exchange backend engineer benefits from compact binary messages when low-latency internal calls matter during trading workflows.
  • A DoorDash logistics engineer can use server streaming to push batch delivery state changes to internal consumers efficiently.
  • A CRED platform engineer may prefer gRPC for typed service boundaries across many JVM and Go services inside the company network.

Pause and recall

  1. Why do protobuf field numbers matter?
  2. What is the difference between unary and server streaming?
  3. When can gRPC deliver better latency than JSON HTTP?
  4. Why should deleted field numbers be reserved?

Interview Q&A

Why choose gRPC over REST for internal services?

gRPC gives typed contracts, compact payloads, code generation, and streaming over HTTP/2. That combination suits high-volume internal service communication well. Common wrong answer to avoid: "gRPC is always faster, so it should replace every HTTP API."

How would you explain protobuf backward compatibility?

Safe evolution comes from keeping field numbers stable, adding new optional fields carefully, and reserving removed numbers. Compatibility is about wire meaning, not only source code names. Common wrong answer to avoid: "Renaming a field is the main risk, field numbers do not matter much."

When would you use bidirectional streaming?

Use it when both client and server need to send messages independently over one long-lived channel. Live speech, chat relay, and collaborative state sync fit well. Common wrong answer to avoid: "Bidirectional streaming is just many unary calls in sequence."

Why not expose every public API as gRPC?

Browsers, external partners, and debugging workflows often benefit from plain HTTP and JSON. Public ecosystems also value simple docs and broad tooling support. Common wrong answer to avoid: "External developers should just install a generated client for everything."


Apply now (5 min)

Exercise. Write a protobuf message for CreateShipmentRequest with five fields. Then define one unary RPC, one server-streaming RPC, and one bidirectional RPC for shipment tracking. For each, name one concrete product use case.

Sketch from memory. Draw client stub, HTTP/2 transport, and server stub. Then label where field numbers live, and where streaming saves repeated request overhead.


Bridge. gRPC is great when both sides already know how to talk. Webhooks are the opposite mood: your system must call someone else later when something happens. → 04-webhooks-and-callbacks.md