01 · Microservices API Design Patterns¶
Once an API is backed by many independently-deployed services rather than one monolith, new design questions appear: who owns which data, how do services talk to each other, and how does a client-facing request survive one of those services being slow or down.
Database-per-service¶
Each microservice owns its own database; no service reaches directly into another's tables.
orders-service → orders_db (orders, order_items)
users-service → users_db (users, addresses)
inventory-service → inventory_db (stock_levels)
orders-service never runs SELECT * FROM users. If it needs a user's
name, it calls users-service's API — exactly like an external client
would. This is what makes services independently deployable: no one can
break another team's queries by changing a table they don't own.
The API composition pattern¶
When a client-facing endpoint needs data owned by multiple services, something has to compose it. Usually an API gateway or backend-for- frontend layer (module 3, Level 3) does this:
{ "order": { "id": 42, "total": 89.99 }, "customer": { "name": "Ada" }, "item_names": ["Widget", "Gadget"] }
Internally this is three calls (orders-service, users-service,
inventory-service) merged by the composing layer, not by the client.
Backend-for-Frontend (BFF)¶
Different clients need different shapes from the same underlying services. A mobile app wants a lean payload; a web dashboard wants everything.
Mobile BFF → GET /mobile/v1/orders/42 → { id, total, status }
Web BFF → GET /web/v1/orders/42 → { id, total, status, items[], customer{}, history[] }
Each BFF is a thin, client-specific layer over the same core services — it avoids either bloating one shared endpoint with every possible field, or forcing every client to do its own composition.
Handling failure: a downstream service is down¶
{
"order": { "id": 42, "total": 89.99 },
"customer": null,
"_warnings": ["customer service unavailable, showing partial data"]
}
A circuit breaker in the composing layer detects users-service
failing repeatedly, stops calling it for a cooldown period (failing
fast instead of piling up slow timeouts), and the composed response
degrades gracefully rather than the whole /orders/42/summary call
failing just because one non-critical field is unavailable.
Saga pattern: distributed transactions without a shared database¶
Placing an order touches three services: reserve inventory, charge payment, create the order record. There's no single database transaction spanning all three. A saga runs it as a sequence of local transactions with compensating actions on failure:
1. inventory-service: reserve 2 units → succeeds
2. payments-service: charge $89.99 → FAILS (card declined)
3. compensate: inventory-service releases the 2 units reserved in step 1
curl -X POST https://internal.example.com/inventory/reserve -d '{"item_id": 5, "qty": 2}'
curl -X POST https://internal.example.com/payments/charge -d '{"amount": 89.99}'
# → 402 Payment Required
curl -X POST https://internal.example.com/inventory/release -d '{"item_id": 5, "qty": 2}'
No global lock, no two-phase commit across services — just a defined compensating action for every step, run in reverse on failure.
Worked example: designing the order-placement flow¶
For an e-commerce platform split into orders, inventory, and
payments services:
- Client calls
POST /v1/orderson the gateway. orders-serviceorchestrates the saga: reserve inventory → charge payment → mark order confirmed.- If any step fails, run compensations for every step that already
succeeded, and return the client a clear error (
402 payment_failed, not a generic500). - On success,
orders-servicepublishes anorder.confirmedevent (module 7) thatinventory-serviceandnotifications-servicereact to independently, rather thanorders-servicecalling them directly and coupling to their availability.
How It Actually Works¶
Patterns like API composition and the backend-for-frontend (BFF) exist to manage a mechanical reality: a single client-facing request often needs data that lives behind multiple independent network hops, each with its own latency and failure mode.
An API composition layer fans a single incoming request out into several backend calls, concurrently:
GET /order-summary/42
-> orders-service.get(42) \
-> users-service.get(order.userId) } issued in parallel, not sequentially
-> inventory-service.check(order.items) /
-> merge all three results into one response
Issuing these calls concurrently (via Promise.all/async gather)
rather than sequentially is the actual mechanism that keeps composed-
endpoint latency close to the slowest single call, not the sum of all
three — a detail that matters enormously at scale, since sequential
composition of three 100ms calls means 300ms, while concurrent composition
means roughly 100ms plus merge overhead.
The circuit breaker pattern is a literal state machine wrapping each
outbound call: CLOSED (calls pass through normally) → after N
consecutive failures → OPEN (calls fail immediately without even
attempting the network call, for a cooldown period) → after the cooldown →
HALF_OPEN (one trial call is allowed through; success closes the circuit
again, failure reopens it). This prevents one slow/failing downstream
service from exhausting the calling service's own thread pool or
connection pool waiting on doomed requests — the breaker fails fast
instead of piling up blocked calls.
Exercise¶
- Why does database-per-service make independent deployability possible in a way that a shared database across services doesn't?
- Design the compensating action for a saga step that sends a confirmation email — is a compensation even needed here, and why or why not?
- A circuit breaker trips on
users-service. What should the composing layer return to the client, and what HTTP status code is appropriate for a partially-degraded response? - Compare API composition (gateway calls multiple services
synchronously) with an event-driven approach (module 7) for keeping
an
ordersread-model in sync withinventorychanges — what's the trade-off?