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.
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.
orders.myservice.com, payments.myservice.com, etc. Every URL change requires an app-store release.// COST: version lock across app releasesEvery 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 terms every "we added a gateway" architecture doc, every AWS API Gateway pricing page, every Kong or Envoy config assumes you already know.
429 Too Many Requests.Test the gateway intuition. Click an answer; explanation drops in instantly.
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.
The edge pattern that lets clients not care about your internal service topology.
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.
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.
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.