InterviewKata
System design · Worked design D38 of 46 · Data and infrastructure · Long form

Design an API gateway

Design the single front door for a company's APIs: every request from the web app, the mobile apps and partner integrations passes through it on the way to about 300 internal services. It must check who's calling, enforce limits, send each request to the right place, and do all of that in a few milliseconds without ever becoming the reason the site is down.

1. The prompt

"Our company runs a few hundred microservices. Design the API gateway that all external traffic goes through."

What makes it hard:

2. Frame the problem

How much traffic?

About 1.5 million requests a second at the daily peak, across three regions.

This sets the size of the gateway fleet, and rules out any shared component that every request must call.

Who calls it?

Our web and mobile apps (users logged in with tokens), and about 20,000 partner integrations using API keys, each on a plan with a quota.

Two kinds of identity, and per-partner limits that are part of a contract.

What must it do, and what not?

TLS, routing, authentication, rate limits and quotas, request IDs and tracing, access logs. No business logic.

The gateway handles concerns every service shares, so teams don't each rebuild them. Business rules in the gateway would make it a bottleneck for every team.

Buy or build?

Assume we build on an open-source proxy (Envoy, NGINX or similar) and design the parts around it: configuration, limits, rollout.

Nobody writes an HTTP proxy from scratch in an interview; the interesting design is the control plane and the policies.

How much latency may it add?

At most 5 ms at p99, not counting the service's own time.

A page that makes 10 sequential API calls would otherwise pay that 10 times.

How often does configuration change?

Hundreds of route or policy changes a day, made by service teams themselves.

Changes must be self-service, validated and rolled out in stages.

3. What it must do

  1. Send each request to the right service (and the right version of it), based on host, path, method and headers.
  2. Check who is calling (a user token or a partner API key), reject what isn't allowed, and enforce rate limits and quotas per caller.
  3. Let service teams change their routes and policies themselves, applied everywhere within seconds.
  4. Record every request: a request ID, a trace, metrics and an access log.

Not now: a developer portal and partner billing, GraphQL federation, bot and DDoS protection (a CDN or edge security layer sits in front of us), service-to-service traffic inside the company (a service mesh's job, see 9.5).

4. How well it must do it

TargetWhy this number
Added latency ≤ 5 ms at p99 speedPaid by every request, often several times per page.
Availability 99.99% or better surviving failuresHigher than any single service, because it's in front of all of them.
Config changes live everywhere within 30 s; a bad change affects a small slice before it's stopped safe changeTeams deploy all day; blast radius matters more than speed.
Fairness: one partner or one slow service can't starve the others isolationShared infrastructure is only safe if neighbours can't hurt each other.

5. Numbers that change the design

QuantityWorking→ so we…
Fleet size1.5 M req/s ÷ ~20,000 req/s per node (TLS, token checks, proxying) ≈ 75 nodes. Each region must survive another region failing, so size each for half the global peak: 750,000 ÷ 20,000 ≈ 38 nodes, plus headroom→ so about 45 stateless nodes per region, ~135 in all, behind layer-4 load balancers
Calls on the request pathA 1 ms network call to a shared store at 1.5 M req/s is 1.5 M calls a second to that store, and 1 ms on every request→ so tokens are verified locally, API keys and routes are cached in memory, and rate-limit counters are local with background syncing
Route table300 services × ~30 routes ≈ 9,000 routesSmall → so the whole table lives in every node's memory, compiled into a prefix tree
Access logs1.5 M req/s × ~1 KB ≈ 1.5 GB/s ≈ 130 TB a day→ so logs are buffered and shipped asynchronously to Kafka, kept hot for 7 days, sampled for long-term storage

Illustrative numbers. A real fleet is sized by load-testing one node with your actual mix of TLS, payload sizes and policies.

6. The nouns

route          id, match {host, path_prefix, methods[], headers{}}, upstream_id, policy_ids[], owner_team
upstream       id, service, discovery (DNS or registry name), lb (least_request), timeout_ms, retry_budget
policy         id, kind (jwt | api_key | rate_limit | cors | transform), settings{}
consumer       id, plan (free | pro | enterprise), api_key_hashes[], quota_per_day, rate_per_sec
config_version n, created_by, created_at, routes+upstreams+policies (whole snapshot), status (canary | live | rolled_back)

7. The contract

Data plane (the internet sees this)
  Any HTTPS request to api.example.com, app.example.com
  Errors in one shape (RFC 9457 problem+json):
    401 no or bad credentials · 403 not allowed · 404 no route
    429 over the limit, with Retry-After · 503 upstream unavailable · 504 upstream too slow
  Adds: X-Request-Id, traceparent, X-Caller (set by the gateway; the same header from outside is removed)

Control plane (service teams and CI use this)
  PUT  /v1/routes/{id}            {match, upstream_id, policy_ids}      → validated, queued for rollout
  POST /v1/consumers              {plan}                                 → consumer id, API key (shown once)
  GET  /v1/config/versions/{n}                                          → status: canary | live | rolled_back
  POST /v1/config/versions/{n}:rollback

Services trust the gateway's X-Caller header only because the gateway strips any copy that arrives from outside, and services accept traffic only from the gateway (or verify a signed internal token).

8. First working version

Two halves, kept apart on purpose. The data plane is a fleet of identical, stateless gateway nodes that handle requests. The control plane stores configuration, validates changes and sends them to the nodes. The data plane must keep working when the control plane is down.

8.1 One request through the gateway

8.2 A team changes a route

Nodes receive whole snapshots, not individual edits, so every node is always on one complete, tested version. Envoy's xDS APIs work this way: the control plane streams configuration to each proxy, and the proxy acknowledges or rejects each version.

Parked for laterkeeping added latency low (9.1) · rate limits across 135 nodes (9.2) · a bad config change (9.3) · one slow service (9.4) · gateway, mesh or BFF (9.5) · regions (9.6).

9. Make it hold up

9.1 Five milliseconds for everything speed

ApproachWhy
✗ BadCall the identity service to check each token, and a database to look up each API keyTwo network calls per request: milliseconds each at p99, and the identity service now needs to take 1.5 M calls a second.
~ GoodCache token checks and key lookups for a minuteFaster on a hit, but every cache miss is still a remote call, and revoked keys keep working for up to a minute.
✓ GreatNothing remote on the request path: verify token signatures locally with the identity provider's public keys (refreshed in the background); push hashed API keys to every node with the config; compile routes into a prefix tree; reuse warm connections to upstreams; write logs to a bufferThe common path is pure CPU and memory. Revocations arrive as a pushed update within seconds instead of waiting for a cache to expire.

Try the trade-offs in the latency lab in section 13.

9.2 Rate limits across 135 nodes fairness

A partner on the Pro plan may make 1,000 requests a second. Their requests are spread over every node in a region, so no single node sees the whole picture.

ApproachWhy
✗ BadEach node allows 1,000 ÷ 45 ≈ 22 a secondNo shared state, but wrong whenever traffic is uneven: a partner with few connections lands on few nodes and gets a fraction of what they pay for.
~ GoodA shared Redis counter, incremented on every requestExact, but adds a network hop to every request, and Redis becomes something every request depends on.
✓ GreatLocal counters, synced in the background: each node counts locally and every ~100 ms adds its count to a shared total in Redis and reads back the global figureNo hop on the request path. The limit can be overshot by about one sync interval's traffic, which is fine for rate limits. Daily quotas (part of a contract) are checked against the shared total.

If Redis is unreachable, nodes fall back to the local share (fail open, with a cap) rather than rejecting everyone. The algorithms themselves (token bucket, sliding window) are in D02.

9.3 A bad change must not take everything down safe change

The gateway's configuration is code that runs on every request for every service. A route with a greedy path pattern, or a policy with a typo, can send all traffic to the wrong place in seconds. Large outages have started exactly this way.

9.4 One slow service isolation

Also per upstream: a timeout shorter than the client's, retry budgets (retries may add at most 10% extra load, so retries can't multiply an outage), circuit breakers and outlier ejection (stop sending to an endpoint that keeps failing). And when the gateway itself is overloaded, it sheds the least important traffic first (a partner's bulk export before a checkout). Module 24 has the patterns.

9.5 Gateway, service mesh or BFF? architecture

Edge API gatewayService meshBackend for frontend (BFF)
TrafficFrom outside into the companyService to service, insideFrom one client type (the iOS app, the web app)
DoesTLS, external auth, partner keys and quotas, routing, request IDsmTLS between services, retries, traffic splitting, telemetry, via a proxy next to each serviceShapes and combines API calls for one client's screens
Owned byA platform teamA platform teamThe client's team
Keep outBusiness logic, data joinsBusiness logicCross-cutting policy (auth, limits)

They are layers, not rivals: a request may pass the edge gateway, then a BFF, then mesh proxies between services. Combining many service calls into one response (aggregation) belongs in a BFF, not in the shared gateway.

9.6 Regions and the control plane surviving failures

9.7 The whole design after the deep dives

10. Interviewer follow-ups

A partner's API key leaks on a public code site.

The partner (or our scanner) revokes it through the control plane; the revocation is pushed to every node as a small update within seconds, without waiting for a full config rollout. Keys are stored only as hashes, and the partner can hold two keys at once so they can rotate without downtime.

How would you route 5% of traffic to a new version of a service?

A weighted route: 95% to the stable upstream, 5% to the canary, sticky by user ID so a user doesn't flip between versions. Promote or roll back by changing the weights.

WebSockets and long-running streams?

The gateway upgrades and proxies them, but they pin a connection to a node for hours. Drain nodes slowly during deploys (stop accepting new connections, let old ones finish or ask clients to reconnect), and count connections, not just requests, when sizing.

Why not let each service do its own auth and limits?

300 teams would implement them 300 slightly different ways, and partners would see 300 behaviours. Services still check authorisation for their own data (is this user allowed to see this order?); the gateway does authentication and coarse access.

Large file uploads?

Don't stream gigabytes through the gateway. Return a pre-signed URL so the client uploads directly to object storage; the gateway only handles the small request that creates it.

The gateway team becomes a bottleneck for every change.

Self-service: teams own their routes in their own repositories; CI validates them against the schema and shared rules (no catch-all paths, timeouts within limits) and submits them; the platform team reviews only changes to shared policy.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Bad config versionErrors on some routesError rate on canary nodes vs the restStaged rollout, automatic rollback, per-route validation
Control plane downNothing (changes are paused)Config age per nodeNodes keep the last good version (static stability)
Rate-limit store downLimits slightly looserSync errorsFall back to local shares, with a cap
One upstream very slowThat feature fails fast; others fineLatency and open connections per upstreamPer-upstream limits, timeouts, circuit breakers
A region lostBrief errors, then slower for far usersHealth checks from outsideDNS or anycast moves traffic; each region sized for half the global peak
Identity provider's keys rotateValid tokens rejectedSpike in 401sFetch new public keys on an unknown key ID; publish new keys before signing with them

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch: added latency (p50, p99) · error rates by route and by cause (gateway vs upstream) · 429s per consumer · config version and age per node · open connections per upstream · rate-limit sync lag.

At 10×: more nodes (they're stateless); shard the rate-limit store by consumer; split the fleet by traffic class (partner API vs first-party apps) so each can be tuned and fail separately.

A 60-minute interview

0–5Frame: traffic, callers, latency budget, buy vs build
5–10Must do, how well, numbers
10–15Nouns and contract, data plane vs control plane
15–25First version: the request path and a config change
25–50Deep dives: latency (9.1), rate limits (9.2), safe rollout (9.3)
50–60Isolation or regions, follow-ups, wrap-up

A 90-minute interview

0–8Frame, including partner plans and quotas
8–15Must do, how well, numbers
15–22Nouns, contract, the two planes
22–35First working version
35–75Deep dives 9.1–9.6 and the final picture
75–85Follow-ups: leaked keys, canaries, uploads
85–90Trade-offs, metrics, 10×
LevelWhat a strong answer shows
Mid-levelA proxy fleet behind a load balancer, routing by path, token checks, a rate limiter, logging.
SeniorData plane vs control plane, nothing remote on the request path, distributed rate limits with their error, per-upstream timeouts and circuit breakers, where a BFF and a mesh fit.
Staff+Configuration as the main outage risk (staged rollout, validation, static stability), self-service for hundreds of teams, traffic classes and load shedding, the cost of every millisecond across the company.

Common mistakes:

13. Try it, check yourself, read more

The latency lab. Choose how the gateway does each job and see what it adds to every request. The line marks the 5 ms p99 budget.

Illustrative numbers for one request inside a data centre. Adding up p99s overstates the true p99 a little; treat the total as a cautious estimate.

How should the gateway check a user's access token on each request?

No network call on the request path; keys are refreshed in the background.

The control plane goes down. What happens to traffic?

Static stability: a control-plane failure stops changes, not traffic.

One service becomes very slow. How do you stop it taking the gateway down?

Bulkheads keep one service's waiting requests from using up everything.

The mobile app needs one call that combines a user's profile, orders and recommendations. Where does that go?

Aggregation for one client belongs to that client's team, not the shared front door.
ResourceWhy
Envoy: dynamic configuration (xDS)How a control plane streams versioned configuration to proxies.
Envoy: global rate limitingLocal and global limits, and when to use each.
Static stability (Amazon Builders' Library)Why the data plane must keep working when the control plane can't.
Handling overload (Google SRE book)Per-client limits, criticality and load shedding.
RFC 9457: Problem Details for HTTP APIsOne error shape for every route.