System design interview

Design an API gateway

Live route tables, local token checks, quotas, retry budgets and breakers.

Overview
ClientGatewayroute, authOperatorKey serviceBackendservicesControlplaneConfig storeversionsClientGatewayroute, authOperatorKey servicetoken, API keysBackend services200 servicesControl planevalidate, pushConfig storeversions

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  1. A client calls the orders API with a signed token. The token names the signing key k7.
  2. The node has no key k7, because it was rotated in a minute ago. It asks the key service for the current public keys.
  3. The keys come back and are cached for 10 minutes. Later requests on this node skip this step.
  4. The node checks the signature and expiry, matches the route, checks the quota, and forwards the call with a trusted identity header.
  5. The backend answers within its 2 second limit.
  6. The gateway returns the response to the client.
  • Client
  • Gateway: route, auth
  • Operator
  • Key service: token, API keys
  • Backend services: 200 services
  • Control plane: validate, push
  • Config store: versions
On this page

In 2 minutes

I would keep the gateway thin and make everything on the request path local. Gateway nodes are stateless and identical, spread over three zones, and at 40,000 requests a second at the peak we need about 8 nodes, so we run 16. Each node holds the route table in memory as a tree of path segments, and the signing keys cached by key id. Tokens are checked by signature, which costs about 50 microseconds, so the whole fleet needs about 2 cores.

A remote auth call would add 2 ms to every request. Keys rotate with an overlap, and unknown key ids are refetched at most once a minute. Partner API keys are stored as hashes and cached. The gateway deletes client-supplied identity headers and sets its own.

Route changes go through a control plane that validates them, replays sample requests, saves a numbered version, canaries it on one node and pushes it, and each node swaps its table in one step. A bad change is rolled back by republishing the old contents as a new version. For protection, each route has timeouts, retries are limited to idempotent calls inside a 10% budget, a circuit breaker fails fast when a backend is sick, and load shedding drops the lowest priority class first. The trade-off is that revoked tokens live until expiry, unless the deny list catches them.

Hide the answers and test yourself

Step 1 of 6: Clarify the scope

Interviewer asks

Design an API gateway. It is the single front door for a company's public API, in front of about 200 backend services. It routes each request to the right service, checks who is calling, applies quotas, and protects the services from overload and from each other's failures.

Questions to ask first

Ask these before you draw anything. Under each one is why its answer changes the design.

Who are the callers: our own apps, partners with API keys, or both?

Own apps carry signed user tokens. Partners carry long-lived keys that need their own storage, quotas and revocation.

How often do routes change, and how fast must a change take effect?

Changes every few minutes need a live push with safe rollback. Changes once a week could use a restart.

Does the gateway only forward, or also reshape and combine responses?

Business logic in the gateway makes it a shared bottleneck that every team has to edit. Keeping it thin keeps it fast and stable.

What does a backend expect to receive about the caller?

The answer sets the identity header we pass on, and the rule that clients must never be able to forge it.

How bad can an outage of one backend be for everything else?

If one slow service may not take down the rest, we need timeouts, limits and breakers per backend.

Requirements

Functional

  • Route a request to a backend by host, path prefix, method and version header.
  • Verify the caller's token or API key, and pass a trusted identity to the backend.
  • Apply a quota to each client, and answer 429 with a retry time when it is used up.
  • Apply timeouts, retries, circuit breaking and load shedding for each backend.
  • Change routes and policies without a deploy, and roll a bad change back.

Non-functional

  • The gateway adds under 5 ms at p99 to a request, with no network call for a token check.
  • A new route table reaches 99% of gateway nodes within 10 seconds.
  • Gateway nodes keep serving with the last good config while the control plane is down.
  • Retries add at most 10% extra load to a backend, however badly it fails.
  • 99.99% availability, with no single node or zone as a single point of failure.

Step 2 of 6: Size it

Everything on the request path must be local: the route table, the signing keys and the quota state live in the node's memory. The network is used only to update them, and to reach the backend.

Assumed for the exercise, not facts about a real system:

  • 40,000 requests a second at the peak, across 200 backend services and 2,000 routes.
  • One gateway node handles about 5,000 requests a second with TLS and token checks. We plan for twice the load, so a zone can fail.
  • A signature check takes about 50 microseconds. A call to a remote auth service takes about 2 ms.
  • A route with its policy is about 500 bytes in the config. Signing keys are cached for 10 minutes.
Gateway nodes
40,000 ÷ 5,000 = 8 nodes; × 2 for headroom
16 nodes, spread over 3 zones
Token checks, local
40,000/s × 50 µs = 2 s of CPU per second
about 2 cores across the whole fleet
Token checks, remote
40,000 calls/s to an auth service, each adding 2 ms
40,000/s of new load and 2 ms on every request
Signing key fetches
16 nodes ÷ 600 s
≈ 0.03 a second, almost nothing
One config push
2,000 routes × 500 B = 1 MB; × 16 nodes
16 MB, so a full table can be sent each time
Worst retry load on a sick backend
A backend taking 4,000/s: 3 retries each means 4 × 4,000 = 16,000/s; a 10% budget means 4,000 × 1.1
16,000/s without a budget, at most 4,400/s with one

Step 3 of 6: Sketch the path of one request

Step through the diagram at the top of the page to watch a request move through these parts.

A Client calls a Gateway node. The node matches the request against its in-memory route table, checks the token or API key, applies the caller's quota, and forwards the call to the Backend services with a trusted identity header. Gateway nodes are identical and hold no state that cannot be rebuilt, so any node can serve any request.

Signing keys come from the Key service. A node fetches them when it meets a key it does not know, and caches them. A token is then checked by arithmetic, with no call out.

An Operator edits routes and policies through the Control plane. It validates the change, saves it as a numbered version in the Config store, and pushes it to the gateway nodes. A node builds the new table beside the old one and swaps to it in one step. The counting behind quotas works like the rate limiter design, so it is not repeated here.

Other paths through the system

A backend starts failing

  1. A client sends a GET for an order. Its token and quota are fine.
  2. The first try to the orders backend times out after 800 ms.
  3. A GET is safe to repeat, and the retry budget has room, so the node tries a different instance.
  4. That one fails too. The breaker for this backend sees more than half of recent calls fail, and opens.
  5. The client gets a fast 503 with a retry time. While the breaker is open, no call reaches the sick backend.

Publish and roll back

  1. An operator submits a new route table that sends 5% of orders traffic to a new version of the service.
  2. The control plane validates the table, then saves it as version 42. The old version stays stored.
  3. It pushes version 42 to one canary node first. The node builds the new table and swaps to it in one step.
  4. The node reports that it runs version 42. The control plane watches its error rate for two minutes.
  5. The error rate is high, so the control plane publishes the contents of version 41 as version 43 to all nodes.

Step 4 of 6: Go deep on the hard part

Route tables, pushed and swapped

Interviewer asks

How do you change routes on 16 gateway nodes without a restart, and survive a bad change?

A route says: if the host, path prefix, method and version header match, send the request to this service, with these timeouts and limits.

Read more: Route tables, pushed and swapped

Keep the whole table in each node's memory, compiled into a tree keyed by path segments, so a lookup costs a few comparisons. The longest matching prefix wins, and an exact match beats a prefix. At 2,000 routes the table is about 1 MB.

The control plane is the source of truth. An operator or a pipeline submits a change. Before saving, the control plane checks that it parses, that no two routes are equally specific for the same request, that each target service exists, and that every pattern is bounded, so a pathological pattern cannot burn CPU. It then replays a set of saved sample requests against the compiled table and compares the targets with what was expected. Only a table that passes becomes a new numbered version in the config store. Versions are immutable.

Delivery. The control plane pushes the version to each node over a long-lived stream, and nodes also poll every 30 seconds in case a push was lost. A node builds the new table next to the live one, validates it, and then swaps one pointer. Requests already running finish on the old table, and the next request sees the new one, so there are no locks and no half-applied state. Each node reports the version it runs, which lets the control plane say that 15 of 16 nodes are on version 42 and name the one that is not.

Safe rollout. Push to one canary node first, and compare its error rate and latency with the others for a couple of minutes. If it is worse, roll back by publishing the old contents as a new, higher version. Never reuse a version number, since a node could not tell which table is newer. A node that receives a table that fails its own checks keeps the old one and reports the problem.

When the control plane is down. The data path does not depend on it. Each node keeps the last good table in memory and on disk, so even a restarted node can start without it. The only cost is that no changes can be made until it returns.

Authenticating at the edge

Interviewer asks

How does the gateway know who is calling without a network call per request, and what about key rotation and revocation?

Signed tokens. A user token is signed by the identity service with a private key.

Read more: Authenticating at the edge

It carries the user, the scopes, an expiry and the key id of the key that signed it. The gateway holds the matching public keys and checks the signature, the expiry (allowing about 30 seconds of clock difference), the issuer and the audience. That is about 50 microseconds, so 40,000 checks a second use about 2 cores. A call to an auth service for each request would add 2 ms and put 40,000 calls a second on one system.

Rotation. Keys are replaced on a schedule. The key service publishes the new public key before the identity service starts signing with it, and keeps the old public key until every token it signed has expired. A node caches keys by id for about 10 minutes. On an unknown key id it refetches, but at most once a minute, and it caches the failure. Without that limit, an attacker could send tokens with random key ids and turn every one into a fetch.

Revocation. Make tokens short-lived, around 10 minutes, so most revocation is just expiry. For an urgent block, such as a stolen account, push a small deny list of user ids and token ids to every node through the same channel as routes. It stays small because entries are dropped once the token would have expired anyway.

API keys for partners. A key is a random secret, shown to the partner once. We store only a hash of it. The gateway hashes the key it receives, and looks the hash up in a local cache with a 60 second lifetime, backed by the key service. Unknown keys are cached for 10 seconds as well, so a flood of guesses does not become a flood of lookups. Compare in constant time, and have the lookup return the partner's plan and limits.

Identity for backends. First, delete any identity header that arrived from the client. This is the classic mistake. Then set headers with the verified user, scopes, client id and a request id, and send them only over a mutually authenticated connection, so a backend can trust that they came from a gateway. Quotas use the plan from the token or key, and count per client in memory, synced between nodes about once a second.

Protecting the backends

Interviewer asks

One backend gets slow. How do you stop it from dragging down the gateway and the other services?

Timeouts. Every route has a time limit, and the gateway passes the remaining time to the backend in a header, so a service deep in a call chain knows when to give up.

Read more: Protecting the backends

Without limits, a slow backend holds connections until the gateway has none left for anything else. Give each backend its own connection pool, a bulkhead, so one cannot starve another.

Retries, carefully. Retry only when it is safe: GET and HEAD, other methods that carry an idempotency key, and failures where the request never reached the service, such as a refused connection. Never retry a POST that may have run. Retry on a different instance, after a short random wait. And set a retry budget: retries may not exceed 10% of the recent requests to that backend. Without a budget, three retries turn the 4,000 requests a second that one backend takes into 16,000 aimed at a service that is already struggling, which finishes it off. With a budget the worst case is 4,400.

Circuit breaker. Track the outcome of calls to each backend. When more than half of the last 10 seconds of calls fail, with at least 20 calls to judge from, the breaker opens and the gateway fails fast with a 503 and a retry time. After 30 seconds it lets a few probe requests through. If they succeed it closes, and if not it stays open. Failing fast gives the backend room to recover and gives the caller an answer in milliseconds, not seconds.

Load shedding by priority. Give each route a class, such as critical, normal and background. Each backend has a limit on requests in flight, found by measurement. When it is reached, reject the lowest class first. Checkout survives while an export job is turned away.

Hedging, optionally. For idempotent reads with a tight p99, send a second copy to another instance if the first has not answered by the p95 time, and cancel the slower one. It costs about 5% extra load and trims the slow tail. Use it only when the retry budget allows, and never for writes.

Step 5 of 6: Name the trade-offs

Checking user tokens

Verify the signature locally, with short-lived tokens over Ask an auth service on every request

No network call, no shared bottleneck and about 50 microseconds. The cost is that a revoked token works until it expires, unless it is on the pushed deny list.

Getting config to nodes

Push numbered versions and swap in memory over Look up the route in a database per request

Lookups are local and the gateway outlives the control plane. The cost is that nodes can briefly run different versions, which is why nodes report theirs.

Retries

Idempotent calls only, within a 10% budget over Retry every failure up to three times

A failing service never sees more than 10% extra load. The cost is that some requests fail that a blind retry would have saved.

What the gateway does

Route, authenticate, limit and protect, nothing else over Also aggregate and reshape responses

A thin gateway is fast, stable and not owned by every team. The cost is that composing several calls has to happen in a backend or a client-facing service.

Step 6 of 6: Say what breaks next

Questions an interviewer asks once the design is on the board. Answer each one out loud first.

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

Add a weighted route, so that 5% of matching requests go to the new target and 95% to the old one. Hash the user id so each user stays on one version. Watch errors and latency for the new target, and raise the weight in steps.

How do you avoid the gateway being a single point of failure?

Run identical, stateless nodes in several zones behind a load balancer, with enough spare capacity to lose a zone. Nothing on the request path needs the control plane or a database. Deploy gateway changes to one node and then to the rest.

What changes for WebSockets and long-lived streams?

Idle and total timeouts are longer, and the stream is counted against a limit of open connections per client. Config swaps do not drop existing streams, and a node being drained waits for them to end or asks clients to reconnect. Authentication happens once, at the start of the stream.

How would you let teams add their own routes safely?

Let each team own a prefix, and have the control plane reject any change that touches routes outside it. Every change goes through the same validation and canary steps. Keep an audit trail of who changed what.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.