Synchronous APIs: REST and gRPC
Design request-response APIs between services, evolve them without breaking clients, and budget latency across call chains.
- Compare REST over HTTP/JSON with gRPC over HTTP/2 and Protocol Buffers
- Evolve an API without breaking consumers: additive changes, versioning and deprecation
- Estimate latency for serial and parallel call chains, and set timeouts
When the Ordering service needs a dish’s price right now, it asks the Menu service and waits. That’s synchronous communication: request, response. The two common styles:
| REST | gRPC | |
|---|---|---|
| Transport | HTTP/1.1 or 2 | HTTP/2 |
| Payload | JSON (human-readable) | Protocol Buffers (compact binary) |
| Contract | OpenAPI document, often optional | .proto file, required, generates client and server code |
| Strengths | universal, easy to debug with curl, great for public APIs | fast, strongly typed, streaming, great between internal services |
1GET /menu/dishes/42 HTTP/1.1
2Host: menu.internal
3Accept: application/json
4
5HTTP/1.1 200 OK
6Content-Type: application/json
7
8{"id": 42, "name": "Spicy Miso Ramen", "price_cents": 1450, "spicy_level": 3}1syntax = "proto3";
2
3service Menu {
4 rpc GetDish (GetDishRequest) returns (Dish);
5}
6
7message GetDishRequest {
8 int32 id = 1;
9}
10
11message Dish {
12 int32 id = 1;
13 string name = 2;
14 int32 price_cents = 3;
15 int32 spicy_level = 4; // added later: old clients simply ignore it
16}Evolving APIs without breaking anyone
Independent deployment only works if one service can change its API without forcing every caller to change at the same moment. The rules for a response that others read:
| Change | Safe? |
|---|---|
| Add a new optional field | ✅ old clients ignore it ( tolerant reader: clients must ignore unknown fields) |
| Remove a field | ❌ clients reading it break |
| Rename a field | ❌ it’s a remove plus an add |
Change a field’s type ("1450" → 1450) | ❌ |
| Make a required field optional (may now be missing) | ❌ for readers that rely on it |
When you must break things, version: /v2/menu/dishes/42 (or a new gRPC service), run v1 and v2 side by side, tell consumers about the deprecation, and remove v1 only once its traffic is gone. In protobuf, never reuse a field number.
Latency adds up
Every synchronous hop adds latency, and a chain is only as available as all its links multiplied together: five services at 99.9% availability each give about 99.5%. When calls don’t depend on each other, run them in parallel - the total is then the slowest, not the sum. And always set a timeout: without one, a slow downstream service ties up your threads until you fall over too.
1menu, customer, stock, payment = 40, 60, 25, 120 # milliseconds
2
3serial = menu + customer + stock + payment
4parallel_then_pay = max(menu, customer, stock) + payment # the three lookups don't depend on each other
5print(f"all serial: {serial} ms")
6print(f"lookups in parallel, then pay: {parallel_then_pay} ms")
7print(f"availability of 4 services at 99.9%: {0.999 ** 4:.4%}")all serial: 245 ms lookups in parallel, then pay: 180 ms availability of 4 services at 99.9%: 99.6006%
Key takeaways
REST (HTTP + JSON) is universal and easy to debug; gRPC (HTTP/2 + protobuf) is fast and strongly typed.
Additive changes are safe; removing, renaming or retyping fields breaks consumers - version and deprecate instead.
Latency and failure compound along synchronous chains; parallelize independent calls.
Every remote call needs a timeout.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: simulate microservice patterns in Python
Build small Python simulations of the patterns - routers, sagas, outboxes, circuit breakers, traces - and run them against sample inputs. They run locally in your browser; no servers or containers needed.
Is this change breaking?
The input has old: field lines, then new: field lines, for a response schema: name type required|optional. Compare them and print one line per difference, in old-field order and then new fields in order:
removed price: BREAKINGprice_cents: type int -> string BREAKINGspicy_level: required -> optional BREAKING(readers relied on it)notes: optional -> required okadded allergens: ok
Then verdict: compatible or verdict: breaking (needs a new version).
- Risky release
- Safe release
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Fit the latency budget
The first input line is the latency budget in milliseconds. Each following line is a stage of the request: serial name=ms (one call) or parallel name=ms name=ms ... (calls made at the same time). Stages run one after another. Print each stage’s cost (stage 1 (parallel): 60 ms, slowest customer), then total: 180 ms of 200 ms - ok or - OVER BUDGET by 25 ms. Finally print the slowest single call: slowest call: payment (120 ms).
- Checkout
- Too slow
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…