Module 33 / 46 · Phase G — Microservices in Practice · 45 min

API
gateways.

You drew the service boundaries in M.32. Now the client has to talk to five services instead of one. The infrastructure pattern that makes that a solved problem — and the second-order failure mode that appears when the gateway starts to become what it was meant to prevent.

// What you'll know by the end

  • The client-side coupling problem
  • The gateway's canonical responsibilities
  • The Backend-For-Frontend (BFF) pattern
  • Anti-patterns and when gateways decay
§ 01 — Six things the client shouldn't have to know

The client is
too smart.

The moment you finish drawing bounded contexts and shipping five services instead of one, a subtle problem appears in your mobile app. The client now knows about all five services. It has to know their hostnames, their protocols, their auth schemes, and how to combine data from three of them into one screen. That's coupling — just on the client side instead of the server side. Every time you split a service, mobile app version 2.3 breaks. Every time you consolidate two services, you break a different version. The client is entangled with your internal topology, and the version-lock problem now runs across an app-store review window instead of a deploy pipeline.

The API gateway exists to solve this — a single client-facing entry point that hides the internal service topology and handles the cross-cutting concerns that would otherwise live in every service (or worse, in the client). Auth, rate limiting, routing, protocol translation, response aggregation, retries, circuit breaking. All at one place. All configurable centrally. The client only knows about one URL; the gateway knows about everything behind it. This is Phase G's first infrastructure pattern because it's the one that makes microservices architectures actually usable by non-server clients. The others (service mesh, discovery, distributed tracing) sit behind the gateway; the gateway is the front door.

// WHAT THE CLIENT KNOWS WHEN THERE'S NO GATEWAY · AND WHY THAT'S TROUBLE
i
Where each service lives. Mobile hard-codes orders.myservice.com, payments.myservice.com, etc. Every URL change requires an app-store release.// COST: version lock across app releases
CLIENT
PAINS
ii
What protocol each speaks. Order service is REST/JSON; Payment is gRPC; Search is GraphQL; Notifications is a WebSocket. Mobile needs four HTTP libraries and two persistent connections.// COST: bundle size, battery, complexity
CLIENT
PAINS
iii
How to auth to each. Order service wants a JWT; Payments wants a signed request; Search wants an API key. Mobile has three credentials to manage and refresh; each service has its own auth code path.// COST: N auth flows instead of 1
CLIENT
PAINS
iv
How to rate-limit itself. Each service publishes its own limits (100/sec, 20/sec, 5/sec). Mobile has to know all of them to avoid getting kicked off — and the limits change independently.// COST: 429s the client should never have to see
CLIENT
PAINS
v
How to compose data. The order-detail screen needs Order + LineItems + Product + Payment + Shipping. Mobile fires 5 parallel requests, waits for all, merges. Mobile is now doing server-side work — badly, over cellular.// COST: 500ms latency + battery drain
CLIENT
PAINS
vi
How to handle partial failure. If the Reviews service is down, does the whole screen fail? What if Payments is slow? Mobile has to implement per-service retry, timeout, and fallback logic — for services it shouldn't even know exist.// COST: complex client-side resilience code
CLIENT
PAINS

Every one of those concerns is a cross-cutting concern — something that affects every request into the system, regardless of which service handles it. Cross-cutting concerns are the classic argument for centralization: solve them once at the edge, or solve them N times in every service and every client. The gateway is the edge. It's where you put the code that would otherwise be duplicated across your entire architecture. The N-services problem becomes a 1-gateway problem, and the client goes from talking to five URLs with three protocols and three credentials to talking to one URL with one protocol and one credential. The internal topology can now evolve without breaking clients — which is, and always was, the actual promise of microservices.

Once you have a gateway, a second-order problem appears — and it's the same shape as the one M.32 warned about. One gateway trying to serve mobile, web, and third-party partners at once does what one Customer class trying to serve Sales, Shipping, and Support does: everything, badly. Each client type has different needs — mobile wants small payloads and composite responses; web wants server-side rendering support; partners want stable versioned contracts and different auth. The BFF pattern (§03) is the DDD move applied to gateways: let each client type have its own gateway, optimized for its needs, owned by its team. Same underlying insight, different layer of the stack. The lesson bounds contexts taught you about services applies again here to gateways.

Every time you split a backend service, three mobile app versions need updates. Every time you consolidate, three more. The gateway is the indirection that stops that.
§ 02 — What a gateway actually does

Eight jobs.
One edge.

A gateway is defined by its responsibilities. Look at any production system that runs one — Netflix Zuul, Kong, AWS API Gateway, Envoy at the edge, Apollo Router — and the same eight capabilities appear, sometimes named differently but functionally identical. Understanding them individually is the first step to reasoning about which gateway to choose, which features to enable, and — critically — which responsibilities do NOT belong in the gateway (that's §05). The list below is the canonical set; a gateway that doesn't do most of these is either very specialized or not really a gateway.

// THE EIGHT CANONICAL GATEWAY RESPONSIBILITIES

// I · IDENTITY
Authentication

Verify who the caller is. Validate JWTs, exchange OAuth tokens, check API keys, terminate mTLS. Done once at the edge instead of N times per service. Internal services get a normalized x-user-id header they can trust because it's set by the gateway, not by the client.

// II · PERMISSIONS
Authorization

Verify what the caller can do. Coarse-grained checks — "this JWT has scope=orders:read, so it can hit GET /orders/*" — happen at the gateway. Fine-grained authorization (business rules, resource ownership) stays in services, because the gateway shouldn't know the domain.

// III · TRAFFIC CONTROL
Rate limiting

Prevent runaway callers from taking down backends. Per-user, per-IP, per-tier (free/pro/enterprise), per-endpoint. Enforced at the edge so services never see the excess load. Token buckets, sliding windows, or Redis-backed distributed counters.

// IV · TOPOLOGY HIDING
Request routing

Map client-facing paths to internal service addresses. GET /api/orders/123 becomes GET http://orders-svc.internal/v2/orders/123. When you split, rename, or version a service, only the routing config changes; clients see the same URL.

// V · PROTOCOL BRIDGE
Protocol translation

Convert between what the client speaks and what the service speaks. REST/JSON in from the client → gRPC out to the service; HTTP/1.1 from a legacy partner → HTTP/2 internally; a WebSocket subscription in → Kafka consumer out. Clients see one uniform surface.

// VI · COMPOSITION
Response aggregation

Combine data from multiple services into one client-facing response. The order-detail page needs Order + Product + Payment + Shipping. The gateway fans out to all four in parallel, composes them into one JSON, returns to client. 1 client-side request instead of 4.

// VII · RESILIENCE
Circuit breaking & retries

Handle backend failures without spilling them to clients. Retry idempotent GETs with exponential backoff. Open a circuit breaker when a service exceeds an error threshold — don't send more requests for a minute. Fail fast instead of amplifying downstream problems.

// VIII · VISIBILITY
Observability

Log, trace, and measure every request. Because every request passes through, the gateway is the natural place to inject request IDs, emit access logs, start distributed traces (spans propagate downstream), and record latency histograms per endpoint. M.36 covers this in depth.

The pattern that runs through all eight: every responsibility answers a "how does the client not care about this?" question. The client doesn't care where services live (routing hides it). The client doesn't care what protocol they speak (translation hides it). The client doesn't care about auth to each service (the gateway authenticates once). The client only cares about the business operation it's trying to perform. That's the philosophical core of the gateway pattern: give the client a clean, stable, business-oriented surface, and absorb all the accidental complexity of "this system is really made of N services" behind it. The specific responsibilities are engineering; the underlying idea is empathy for the client.

Two implementation notes worth calling out. First, most modern gateways are proxy-based, not application-based: they're deployed as sidecar-adjacent processes (Envoy, HAProxy, Nginx-plus) or hosted services (AWS API Gateway, Cloudflare, Kong) rather than as long-lived application servers that developers write handlers in. This is a good trend — gateways that developers customize with business logic slowly become mini-monoliths (§05). Second, the response aggregation responsibility is where GraphQL federation lives: instead of writing custom aggregation logic per endpoint, each service exposes a GraphQL subgraph, and a federated router (Apollo Router, Wundergraph, Cosmo) stitches them into one graph the client queries. Same pattern, more declarative implementation. Both approaches ship in production; the pick depends on how much aggregation logic you have and how much your clients want to shape their own queries.

Cross-cutting concerns cross every service. Solve them once at the edge, or solve them N times per service — and pay the coordination cost forever.
§ 03 — Backend-for-Frontend · the DDD move at the edge

One gateway
per client type.

The naive gateway architecture is one gateway serving every client — mobile, web, third-party partners, internal admin tools. It looks reasonable on Day 1. Then Month 3 arrives and the mobile team wants a smaller composite payload for the order screen; the web team wants server-side-rendering-friendly HTML fragments; the partner integration team wants a strict versioned OpenAPI spec with a 12-month deprecation guarantee. The single gateway can't satisfy all three without becoming three different products in one codebase. Whichever team gets prioritized wins; the others build workarounds; the gateway becomes contested territory. The pattern that fixes this — Backend For Frontend (BFF), coined by Phil Calçado and popularized by Sam Newman — is directly analogous to bounded contexts. One gateway per client type. Each optimized for its client. Each owned by the team that owns that client. Different clients have different bounded contexts; the gateways should reflect that.

// THREE CLIENTS, THREE GATEWAYS · EACH OWNED BY ITS CLIENT'S TEAM

MOBILE APP iOS + Android WEB SPA React / Next PARTNERS 3rd-party API MOBILE BFF small · composed WEB BFF SSR-friendly · hydration PARTNER API versioned · OAuth · stable SHARED BACKEND SERVICES (bounded contexts from M.32) Sales Inventory Payments Shipping Reviews Auth 6 services, 3 BFFs consume differently shared internal topology tailored client-facing surface
// MOBILE BFF
Small & composed

Optimized for cellular networks and battery life. Trims fields to what the mobile UI actually renders. Pre-composes screens (order + line items + payment status in one response). Ships small images pre-sized for mobile viewports.

payload → <100KB · rounds → 1 · TTFB < 200ms
// WEB BFF
SSR & hydration

Optimized for server-side rendering and progressive enhancement. Returns HTML fragments for initial paint plus JSON for hydration. Different auth (session cookies + CSRF), different cache strategies, different composite payloads. Owned by the web team.

HTML + JSON · session auth · caching tiers
// PARTNER BFF
Versioned & stable

Optimized for third-party integration stability. Strict versioned OpenAPI spec. OAuth 2.0 client credentials flow. 12-month deprecation guarantees. Insulated from internal changes — partners get a slow-moving public contract while internal services evolve normally.

OpenAPI · OAuth · SLAs · deprecation policy

The BFF pattern's core insight is that a gateway serving multiple client types is the same anti-pattern as a service serving multiple bounded contexts. The mobile team's needs and the partner team's needs are actually different domains — different ubiquitous language ("checkout session" vs "commerce transaction"), different rate-of-change (mobile ships weekly; partner API ships quarterly), different failure modes (mobile tolerates delays; partners have SLAs). Forcing one gateway to serve both is exactly the same mistake as forcing one service to serve Sales and Shipping. The DDD move applies: give each context its own home. One gateway per client type; one team per gateway; each free to evolve at its own pace. The backend services themselves are shared (they're where the bounded contexts live); the gateways are the client-facing translation layer.

An honest observation about when NOT to build separate BFFs: if you have only one client type, don't. A pure API product (Stripe, Twilio, Cloudflare) has partners as its only client — one gateway is correct. A pure web application with no mobile app is fine with one gateway. The BFF pattern earns its keep when you have genuinely different client contexts with different needs; if the "difference" is just "mobile and web both call the same JSON API," you don't have three contexts, you have one. Over-applying the pattern produces three gateways that all do the same thing — which is worse than one gateway that does it fine. As with all patterns from Phase G: fit-for-context, not cargo-cult.

One gateway for mobile, web, and partners does what one class for Sales, Shipping, and Support does: everything, badly.
§ 04 — Order detail request · three architectures

Same request.
Three architectures.

Below: a mobile client fetching an order detail page. Six backend services need to respond — Order, Product, Payment, Shipping, Reviews, Customer. Toggle between three architectures: no gateway (mobile calls each service directly), generic gateway (one gateway routes and aggregates for all clients), mobile BFF (dedicated mobile-optimized gateway with response composition and graceful degradation). Then flip failure toggles below the diagram to see how each architecture handles partial backend failures. Watch the client-side request count, the payload on the mobile wire, and how each architecture degrades.

GATEWAY.SIM // m.33 lab
Fail services →
// REQUEST FLOW · order detail page
// METRICS · MOBILE WIRE
Client requests-
Round-trips (RTT)-
TTFB latency-
Payload on mobile-
Auth calls-
Failure handling-
// VERDICT
Loading...
...
§ 05 — Where gateways go wrong

The gateway that
ate everything.

The gateway pattern's biggest risk is its biggest strength turned against you. Because every request passes through, the gateway is a natural place to add "just one more thing" — a bit of business logic here, a small aggregation there, a special auth flow for that one partner. Over months and years, the gateway accumulates responsibilities that don't belong to it, and it slowly becomes the mini-monolith that microservices were supposed to eliminate. The same pattern that made microservices manageable becomes the coordination bottleneck that makes them slow again. The five anti-patterns below are the ones that reliably appear in gateways that started well and degraded. Recognizing them early is the difference between a healthy gateway and a mid-2020s Zuul rewrite project.

// FIVE WAYS GATEWAYS DEGRADE INTO PROBLEMS

i
The fat gateway
"Just add this business rule to the gateway — it's the only place all requests pass."

Business logic starts creeping in — pricing rules, feature-flag checks, personalization, entitlement logic. Six months later, the gateway has more domain code than most services. The rule: gateway does routing, auth, rate limiting, protocol translation, and generic aggregation. Business logic lives in services. If you find yourself writing "if customer_tier == premium" in the gateway, that check belongs in Sales or Pricing, not here.

ii
The deploy bottleneck
"Every service change requires a gateway config update. The gateway team is a queue."

When every new endpoint requires the gateway team to update routing config, add auth policy, and write aggregation logic, the gateway becomes the release bottleneck for every service. Services can deploy independently but their public API can't. The fix: self-service gateway config (each service owns its routing config, gateway reloads dynamically), or move to GraphQL federation where each service publishes its own schema.

iii
The single point of failure
"Gateway is down. Everything is down."

Every request goes through it, so gateway outages take down the entire product. An unavailable gateway is worse than any single unavailable service because it eliminates the graceful-degradation benefit the gateway was supposed to provide. The fix: multi-region active-active gateway deployment, aggressive health checks, and — crucially — a client-side fallback that lets read paths continue through a static CDN cache when the gateway is unreachable.

iv
The one-gateway-fits-all
"Mobile, web, partners, and the internal admin tool all hit the same gateway."

The BFF pattern from §03 exists because different clients have genuinely different needs. One gateway trying to serve them all becomes contested territory — mobile wants tiny composed payloads, web wants SSR-friendly HTML, partners want a stable versioned contract. Every feature request pulls the gateway in a different direction. The fix: split by client type. One BFF per team that owns a client.

v
The latency tax
"P99 crept from 200ms to 800ms this quarter. Nobody knows why."

Every added feature adds latency. Auth plugin: +5ms. Rate limiter with Redis check: +8ms. Custom header rewrite: +2ms. Fifteen features later, the gateway alone adds 100ms to every request before the backend even starts working. The fix: budget latency per feature, run continuous P99 tests, delete plugins that don't earn their overhead. Gateways are on the hot path — treating them like sacred infrastructure means never optimizing them.

The composite advice: the gateway should be boring. Not glamorous, not creative, not clever — boring. Its job is to route, authenticate, rate-limit, translate, aggregate, and observe. When engineers describe their gateway with exciting adjectives ("smart," "adaptive," "intelligent"), that's usually a sign it's doing more than it should. The healthiest gateways are ones that look identical to gateways at other companies solving similar problems, because the responsibilities are well-understood commodity infrastructure, not a place to differentiate. Save the interesting engineering for the services behind it.

The tools people converge on reflect this. Envoy (originally from Lyft, now the CNCF core) powers a huge chunk of edge gateway deployments and is the data plane inside Istio's service mesh (M.34). Kong and Tyk are the most common self-hosted API gateways with strong plugin ecosystems. AWS API Gateway, Google Apigee, and Azure API Management are the hosted equivalents. Apollo Router and Cosmo are the current best-in-class for GraphQL federation. All of these do essentially the same job with different operational tradeoffs. The pick is usually about your team's ops preference and integration surface, not about capability — because at this layer, the capabilities are commoditized. And that's the point.

The gateway that started as routing becomes the mini-monolith you can't deploy without a change-control board. Save the interesting engineering for the services behind it.
§ 06 — Eight words for the gateway conversation

Vocabulary,
for the edge.

The terms every "we added a gateway" architecture doc, every AWS API Gateway pricing page, every Kong or Envoy config assumes you already know.

API Gateway
/eɪ piː aɪ ˈɡeɪt.weɪ/
A single client-facing entry point that handles auth, rate limiting, routing, protocol translation, and response aggregation for a set of backend services. Hides internal topology from clients. Runs on the request hot path.
BFF · Backend For Frontend
/biː ɛf ɛf/
A pattern where each client type (mobile, web, partners) has its own dedicated gateway, optimized for that client's needs and owned by that client's team. Coined by Phil Calçado at SoundCloud (~2015); popularized by Sam Newman. The gateway analogue of bounded contexts.
Response Aggregation
/rɪˈspɒns/
The gateway responsibility of fanning out to multiple backend services and composing their responses into one client-facing payload. Turns N client requests into 1. The mobile-friendly alternative to letting the client do the fan-out itself.
Circuit Breaker
/ˈsɜːr.kɪt ˈbreɪ.kər/
A pattern where the gateway stops calling a failing backend service for a configured window after exceeding an error threshold. Prevents cascading failures. Named after electrical circuit breakers; popularized by Michael Nygard's Release It! and Netflix Hystrix.
Rate Limit
/reɪt ˈlɪm.ɪt/
A gateway-enforced ceiling on how many requests a caller can make per unit time. Implementations: token bucket, sliding window, fixed window. Dimensions: per-IP, per-user, per-tier, per-endpoint. Returns HTTP 429 Too Many Requests.
GraphQL Federation
/ˌfɛd.əˈreɪ.ʃən/
A pattern where each service exposes a GraphQL subgraph and a federated router stitches them into one unified graph the client queries. Declarative alternative to writing custom REST aggregation. Apollo Federation, Wundergraph, Cosmo are the leading implementations.
TLS Termination
/tiː ɛl ɛs/
The pattern of decrypting HTTPS at the gateway and forwarding plaintext HTTP internally. Centralizes cert management and lets services skip TLS overhead. Combined with mTLS between gateway and internal services when zero-trust is required (M.34).
Fat Gateway
/fæt ˈɡeɪt.weɪ/
The anti-pattern where business logic creeps into the gateway, turning what should be a boring routing layer into a mini-monolith with domain code, feature flags, and deploy dependencies. The gateway analogue of the distributed monolith from M.32.
§ 07 — Knowledge check

Five questions.
Route the answers.

Test the gateway intuition. Click an answer; explanation drops in instantly.

QUESTION 1 OF 5
Loading question...
Score: 0 / 5
5 / 5

Routed.

Perfect. Gateway responsibilities, BFF pattern, anti-patterns — all yours. Now: how do the services BEHIND the gateway find each other, talk securely, and get consistent observability? That's the service mesh.

§ 08 — The recap

Three ideas to
carry forward.

The edge pattern that lets clients not care about your internal service topology.

i

Gateways absorb cross-cutting concerns

Auth, rate limiting, routing, protocol translation, response aggregation, circuit breaking, observability. Solve them once at the edge instead of N times per service — and let clients treat the whole system as one URL.

ii

One BFF per client type

Mobile, web, and partners have different needs. Different payload sizes, different auth, different failure tolerance, different release cadence. One gateway serving all three does none of them well. The DDD move applied to the edge.

iii

Keep it boring

Business logic in the gateway is the fat-gateway anti-pattern. Every added responsibility is a latency tax and a deploy dependency. Save the interesting engineering for the services behind it.

↓ UP NEXT

M.34 — Service
mesh.

The gateway handled north-south traffic (clients ↔ services). But your services still talk to each other — east-west — and every one of them needs mTLS, retries, circuit breaking, distributed tracing, and traffic shaping. The pattern that gives you all of that without touching your application code.

Continue to Module 34 →