Skip to content
advanced Phase 10 · API Architecture and Contracts

API Gateways and Backend-for-Frontend

Terminate cross-cutting concerns at the edge and tailor APIs per client.

55m
0 problems
Topic Progress 0%

What the Edge Owns: Uniform Policy, Never Business Rules

Twenty services, each doing its own TLS, its own token validation, its own rate limiting — with twenty implementations, twenty bug surfaces, and one team that fixed theirs properly. The gateway exists to do those things once, for everyone: terminate TLS, validate tokens, apply quotas, route to the right backend, and log uniformly.

Concern Done at the gateway Why there
TLS termination yes one certificate lifecycle; services speak plaintext or mesh-internal mTLS
Authentication yes a bad or expired token dies at the edge, before any service spends a thread
Rate limiting and quotas yes cheap enforcement before the expensive work happens
Request routing yes path, host, header, or weighted rules drive canaries and rollouts
Circuit breaking / outlier ejection yes one place to fail fast against a struggling upstream
WAF, CORS, request-id injection, access logs yes uniform edge policy and one trace to correlate all services
Fine-grained authorization no the gateway knows a token is valid, not whether this user may refund this order
Business orchestration no multi-service workflows need transactions, domain tests, and domain owners
Session state and long-running jobs no state makes the gateway unscaleable and its deploys dangerous

The dividing rule is worth memorizing: the gateway enforces policy that is identical across every API; anything that changes when the business changes belongs in a service. Authorization decisions, pricing rules, and workflow steps in the gateway create a tier owned by nobody, deployable by everybody, and untestable without the whole system up. It becomes the place where every product request queues behind a platform release.

Placement also decides failure semantics. A gateway is a single failure domain in front of all your services, so it must be stateless, multi-AZ, and scaled well beyond current load, with configuration pushed from a control plane rather than edited on instances. Health checks, graceful connection draining, and rolling restarts are not optional extras — every client's outage is now your outage. When someone asks what happens if the gateway goes down, the honest answer covers redundant instances behind a load balancer (this is why its own scaling matters), clients that back off rather than hammer, and the DNS or anycast failover path.

Do not confuse edge with origin: caching product pages or serving stale reads during a dependency outage is a graceful-degradation concern applied at the edge, not a reason to move logic there. And keep per-client shaping out of the shared gateway tier — the mobile payload problem has its own pattern, next: when one universal API stops serving every client, teams split the edge per client type rather than growing a switchyard of conditional parameters. The mechanism itself picks up from GraphQL Trade-offs, where one client-shaped endpoint already showed the pull toward per-client APIs.

Implementation choices are worth naming: managed offerings (AWS API Gateway, Google Apigee), reverse proxies with plugin ecosystems (Kong, APISIX, NGINX with Lua, Envoy), or a library-level edge when you only need routing. They differ in latency, plugin model, and how configuration reaches instances — all of which matter, because a gateway adding 30 ms to every call taxes your entire p99. Measure the edge: it sits on the hot path of every request, so its own performance is architecture, not plumbing.

The authentication boundary also needs a precise contract with services. The gateway verifies signature and expiry, then passes standardized claims downstream as trusted headers or an internal token, and services treat those claims as authoritative. If any service also accepts the raw client token, you now have two validation paths drifting apart — the gateway checks expiry, the service forgets — and you end up debugging what looks like clock skew during an outage.

Shared API or Backend-for-Frontend: The Mobile Home-Screen Problem

The mobile home screen needs profile, notifications, and three featured items. Against the shared API it fires three requests: 600 ms of sequential round trips on a 200 ms link, downloading forty fields to render five. The web app copes — more bandwidth, a team that aggregates client-side — and the partner integration wants entirely different, coarser operations. One universal API now serves all three, and it shows: ?fields=, ?include=, and ?view=mobile accumulate until nobody can name every combination that must keep working.

The backend-for-frontend pattern answers with one tailored API per client type: a mobile BFF, a web BFF, a partner API. Each composes the domain services into exactly the shape one client renders.

Shared universal API Backend-for-Frontend
Payload lowest common denominator + parameters exactly what the screen renders
Round trips client aggregates composition happens server-side
Ownership platform team bottleneck the client team owns its BFF
Cost one deployment N codebases, N deploys, N tests
Failure mode parameter explosion, untested combos contract divergence, duplicated logic

The discipline is in the boundaries. A BFF composes and shapes — it fans out to services, merges results, trims fields, sets response headers for its client — but it must not own domain logic: no pricing rules, no inventory decisions, nothing whose correctness the next client also depends on. Cross that line and you have built a distributed monolith with a friendly name. Size it by client type, never by client instance: a BFF per user or per feature is the trap that produces dozens of unmaintainable edge services. Two or three BFFs with clear owning teams is the healthy shape.

How this interacts with the query-shaped flexibility from GraphQL Trade-offs is a real design decision, not a rivalry: a BFF can expose a GraphQL schema tailored to its client, or serve plain JSON endpoints that already match the screen. Pick one mechanism per client — teams that adopt GraphQL and a hand-built composition layer and a gateway transformation for the same flow end up maintaining three contracts for one screen.

Two practical rules keep BFFs cheap. First, keep them thin: stateless services with straightforward HTTP calls to the domain tier, so they scale like the stateless tier of Vertical vs Horizontal Scaling. Second, version nothing inside them if you can avoid it — a BFF ships with its client, so it can change in lockstep with the app release that consumes it; that co-release ability is the pattern's entire economic argument. The API versioning burden stays on the stable domain contracts instead.

Ownership decides whether the pattern survives: the team that ships the mobile app should own the mobile BFF, because the API shape changes when their screen does, and waiting on a platform backlog defeats the purpose. That requires domain teams to treat each BFF as a first-class consumer with stable contracts and clear SLAs — otherwise every BFF starts reading databases directly, and three edge services now hold private data-access paths nobody governs.

Watch for the duplication signal: the same aggregation appearing in two BFFs means either the domain service should offer it, or one client genuinely needs it. Extracting fan-out into the service tier keeps BFFs thin; leaving it duplicated is how BFFs quietly become the new monolith you split out to avoid. Since a BFF's job is transformation, test it accordingly — snapshot the composed response for each client screen and fail CI when a domain change alters one, so the client team sees the impact in their pipeline instead of in an app-store review.

Containing a Slow Inventory Service: Timeouts, Retry Budgets, and Outlier Ejection

Inventory starts answering in 5 seconds instead of 50 ms. Without edge controls the cascade is mechanical: gateway threads pile up waiting, capacity for healthy routes drains alongside the sick one, clients time out and retry — multiplying load on the very service that is struggling — and within minutes the whole site is down because of one slow dependency. Everything below exists to break that chain.

Timeouts must shrink as the call travels deeper. A workable ladder: client 10 s, gateway route 8 s, service 5 s, database query 2 s. Each layer therefore fails with a real error while the layer below can still respond, instead of the outermost layer giving up on a call that is still consuming resources. Propagate the deadline (and a request id) across hops so the innermost service can stop working on a request nobody will collect.

Retries at the edge need a budget. Only idempotent methods, only on connection failures and 503, capped at one or two attempts with jitter — plus a retry budget that refuses retries beyond a small percentage of live traffic. Without the budget, a gateway retry policy converts a 10% backend failure into a 20% surge exactly when the backend is weakest. The idempotency requirement is the same rule you met in REST Design and Versioning; the edge is where violating it hurts most.

Outlier ejection is the circuit breaker's edge form. Track consecutive 5xx or a sliding success rate per upstream instance; when it trips, eject the instance from the load-balancing pool for a cool-down, fail fast with 503 and Retry-After, and probe gradually as it recovers. One broken pod stops receiving traffic without a human in the loop.

Symptom at the edge Control Client contract
One route saturating per-route concurrency cap (bulkhead) 429/503 early, before queues explode
One instance failing outlier ejection 503 + Retry-After, traffic shifts
Upstream slow, not failing timeout ladder deadline error, threads freed
Client calling too hard per-key token bucket 429 + Retry-After with a clear quota message

Add the last two layers deliberately: circuit breaking and rate limiting at the gateway contain downstream failures instead of importing them, and request queuing should be bounded — reject early rather than accept work you cannot finish. Then instrument per-route, per-upstream golden signals (latency, traffic, errors, saturation), because a gateway without per-upstream visibility is just a very expensive single point of failure.

Finally, design the degradation path: when inventory is open but slow, the edge can serve a cached product page and mark availability unknown rather than fail the whole request. That is the same containment philosophy one phase onward — degrade the optional, protect the core path — which is precisely what Timeouts and Retries formalizes next.

Rehearse the failure modes before they rehearse you. An open circuit must return 503 with Retry-After — a contract your client SDK has to honor, or the fast failure merely shifts the load pattern. Ejection needs hysteresis and cool-downs, or half the fleet bounces during one transient blip. And rejection must always arrive with telemetry: per-upstream error budgets let the alert fire while there is still capacity to absorb it.

Deploys are their own failure mode. A gateway that resets connections on rollout converts a routine release into a burst of client errors; graceful shutdown plus slow endpoint removal from DNS costs an afternoon and removes the most common self-inflicted outage at the edge. Drain in-flight requests, watch the connection count reach zero, then exit.

Quiz

1. A reviewer proposes putting the multi-step order-pricing workflow in the API gateway so every client gets it for free. What is the correct response?

Question 1 options

2. Which symptom most directly justifies a backend-for-frontend over extending the shared API?

Question 2 options

3. With a client timeout of 10 s, a gateway route timeout of 8 s, and a service budget of 5 s, what happens when the service hangs until its own timeout at 5 s?

Question 3 options

4. A gateway retries every 5xx twice on all routes during an upstream outage. What is the primary risk, and what is the standard mitigation?

Question 4 options

Flashcards

Question

What belongs at an API gateway, and what must stay out?

Answer

In: TLS termination, token authentication, rate limiting/quotas, routing (including canary weights), circuit breaking/outlier ejection, WAF/CORS, request-id and access logging — anything uniform across every API. Out: fine-grained authorization, business orchestration, session state, long-running jobs — anything that changes when the business changes, because it makes the edge an unowned, untestable tier.

Question

What is a backend-for-frontend, and what keeps it from becoming a distributed monolith?

Answer

One tailored API per client *type* (mobile, web, partner) that composes domain services into exactly the shape that client renders, owned by the client's team. It stays thin — shaping, fan-out, trimming, response headers only — with no domain logic inside; it is sized by client type, never per user or per feature; and it ships in lockstep with the client release it serves.

Question

Why should timeouts shrink as a request travels deeper?

Answer

Client > gateway > service > database (e.g. 10s > 8s > 5s > 2s) ensures the innermost layer fails first with a real error the layers above can relay, while outer layers never abandon work that continues consuming resources. Inverting the ladder produces orphaned requests and gateway timeouts that say nothing about the actual cause.

Question

What is a retry budget and why does edge retry policy need one?

Answer

A cap expressing retries as a percentage of live traffic, layered on rules restricting retries to idempotent methods and transient statuses (connection failures, 503) with jittered backoff. Without it, a modest backend failure multiplies into a retry storm at exactly the wrong moment; the budget keeps some retry value while bounding amplification.

Question

How does outlier ejection at the gateway relate to the circuit breaker pattern?

Answer

It is the same pattern applied per upstream instance at the edge: consecutive 5xx or a sliding success rate marks an instance unhealthy, it is ejected from the pool for a cool-down (open), traffic fails fast with 503 + Retry-After, then limited probes test recovery (half-open). It removes a broken pod without a human or a redeploy.

Revision Notes

Key Takeaways

  • 1. The gateway owns uniform edge policy — TLS, authn, quotas, routing, breaking — while authorization depth and business orchestration stay in services that own the tests
  • 2. A backend-for-frontend is one tailored API per client type, thin enough that it shapes rather than decides, and co-released with the client it serves
  • 3. Parameter explosion on a shared API is the signal to split per client; fat BFFs duplicating domain logic is the signal to consolidate
  • 4. Timeouts shrink with depth and retries carry a budget — otherwise the edge amplifies the outage instead of containing it
  • 5. The gateway is a single failure domain: stateless, multi-AZ, control-plane configured, drained gracefully, and instrumented per route and per upstream or it is just a very expensive single point of failure

Interview Tips

  • • "What does an API gateway do, and what should it not do?" — list the uniform policy set (TLS, authn, quotas, routing, breaking), then explicitly exclude business logic and fine-grained authorization with the ownership rule as justification
  • • "How would you design APIs for mobile, web, and partners?" — BFF per client type with owning teams, thin boundaries, co-release economics; then name the failure mode (contract divergence) to show you weighed the cost
  • • "A dependency gets slow — walk me through what the edge does" — timeout ladder with deadline propagation, retry budget on idempotent calls only, outlier ejection to 503 + Retry-After, per-route bulkheads, then degraded serving of cached reads
  • • "Isn't the gateway a single point of failure?" — say yes, then answer with the properties: stateless multi-AZ fleet, control-plane config, health checks and graceful draining, over-provisioned for failure spikes, per-upstream observability

Cheat Sheet

API Gateways and Backend-for-Frontend

Ownership rule: the gateway enforces policy identical across every API; anything that changes when the business changes belongs in a service.

Gateway does Gateway must not do
TLS termination, token validation fine-grained authorization (who may refund this order)
Rate limits, quotas, per-key throttling business orchestration and transactions
Routing, weighted canaries, redirects session state, long-running jobs
Outlier ejection, circuit breaking per-client payload shaping (that's the BFF)
WAF, CORS, request-id, access logs domain rules, pricing, workflow steps

BFF: one tailored API per client type, owned by the client team, thin (compose + shape), no domain logic, co-released with its client. Failure modes: parameter explosion (shared API) vs contract divergence (too many fat BFFs).

Failure containment ladder:

timeouts      client 10s > gateway 8s > service 5s > db 2s, propagate deadline
retries       idempotent only, transient statuses, jitter, retry budget (max % of traffic)
outliers      consecutive 5xx / success-rate window -> eject instance -> 503 + Retry-After
bulkheads     per-route concurrency caps -> 429/503 early
rate limits   token bucket per key -> 429 + Retry-After

Gateway survivability: stateless, multi-AZ, over-provisioned, config from a control plane, health checks + graceful draining, per-route/per-upstream golden signals. It is a single failure domain in front of everything — treat it that way.

Continue Learning