All articles
Design PatternsSystem DesignArchitecture12 min read

API Gateway: One Front Door for Many Services

Without a gateway every client must understand your service topology and every service reimplements auth and rate limiting. What belongs in the gateway, what must never go in it, and how BFF fits.

The problem: clients know too much

With no gateway, a mobile app opens connections to catalog, orders, payments, search and profile directly. That means the app hard-codes five hostnames, implements retry policy five times, and has to ship an update whenever you split a service in two.

Meanwhile each service has written its own JWT verification, its own rate limiter and its own request logging — five implementations, of which at least one has a bug.

WebMobilePartner APIAPI GatewayTLSAuthNRoutingRate limitObservabilityCatalogOrdersPaymentsSearchProfile
One edge component owns everything that is true for every request.

What belongs in a gateway

ConcernWhy the edgeNotes
TLS terminationOne certificate lifecycleRe-encrypt internally if required
AuthenticationReject unauthenticated earlyValidate token, pass verified claims
RoutingClients address paths, not hostsEnables service splits without client releases
Rate limitingProtects everything behind itPer key, per IP, per route
Request/response shapingHeader hygiene, compressionKeep it mechanical
ObservabilityOne place with the whole pictureTrace id injection belongs here
Canary and A/B routingTraffic splitting without redeploysWeighted routes

What must never go in it

The gateway is shared infrastructure. Anything you put in it becomes a deployment dependency for every team.

  • Business logic. Discount rules in a Lua filter is how you get an outage during a sale that nobody can debug.
  • Authorisation decisions. The gateway can authenticate — prove who you are. Whether user X may refund order Y depends on domain state the gateway does not have.
  • Data transformation with domain meaning. Renaming a field is fine; deriving totals is not.
  • Long-running work. Anything slow at the edge consumes connections that all traffic shares.
The test
If a product manager could request a change to it, it does not belong in the gateway.

Backend for Frontend

One gateway serving a web app, a mobile app and partner integrations eventually becomes a pile of client-specific conditionals. The BFF pattern gives each client class its own thin gateway.

  • Mobile BFF returns compact payloads and aggregates aggressively to save round trips.
  • Web BFF returns richer objects and can rely on better connectivity.
  • Partner API is versioned, contractual, and changes at a very different pace.

Each BFF is owned by the client team, which removes the cross-team queue that a single shared gateway creates.

Aggregation and its trap

Combining three calls into one is genuinely valuable on mobile networks, where a round trip can cost 200ms. But an aggregating endpoint inherits the availability of everything it calls.

GET /bff/home
  ├─ catalog.featured()      required
  ├─ profile.summary()       required
  └─ reco.forUser()          optional, 150ms budget

// three sequential calls: 90 + 40 + 150 = 280ms
// parallel with per-call budgets:      max(90, 40, 150) = 150ms

return {
  featured, profile,
  recommendations: recoResult ?? null,   // degrade, do not fail
  degraded: recoResult ? [] : ["recommendations"],
}
Partial responses keep the page useful when one dependency is degraded.

Fan out in parallel, give each dependency its own timeout, and mark optional sections as degraded rather than failing the whole response.

Authentication at the edge

  1. Gateway validates the JWT signature, expiry, issuer and audience.
  2. It strips any client-supplied identity headers — otherwise a caller can forge X-User-Id.
  3. It injects verified claims as internal headers, or forwards the token itself.
  4. Services still verify authorisation. Never assume “internal traffic is trusted”; that assumption fails the day one service is compromised.

Configuration sketch

routes:
  - path: /api/orders/*
    service: orders-svc
    auth: required
    timeout: 3s
    retries: 0                 # non-idempotent, never retry blindly
    rateLimit: { key: user, limit: 100, window: 1m }

  - path: /api/catalog/*
    service: catalog-svc
    auth: optional
    timeout: 800ms
    retries: 1
    cache: { ttl: 60s, vary: [locale] }
    rateLimit: { key: ip, limit: 1000, window: 1m }

  - path: /api/payments/*
    service: payments-svc
    auth: required
    timeout: 10s
    retries: 0
    rateLimit: { key: user, limit: 10, window: 1m }
Declarative routes keep the gateway boring, which is the goal.

The single point of failure

Everything now depends on the gateway, so treat it as a tier-0 system.

  • Run it stateless and horizontally scaled across zones.
  • Keep the config in version control with staged rollout — most gateway outages are config.
  • Watch added latency separately from upstream latency; the gateway's own p99 is a metric.
  • Do not let it become the deployment bottleneck for every team's route change.

Interview framing

“All clients hit a stateless gateway that terminates TLS, validates the JWT, strips forged identity headers, applies per-user rate limits and injects a trace id. Routing is declarative so we can split services without shipping a client. Authorisation stays in the services because it needs domain state. Mobile gets its own BFF for aggregation with per-dependency timeouts and partial responses.”

Keep reading

Suggested next articles based on this one.

Design it, don't just read it.

Practise LLD and system design problems with structured rubrics and AI feedback.

Start practising free