API Gateway Interview Questions | JiQuest

add

#

API Gateway

System design deep dive · HLD

Design an API Gateway: full high-level design.

Requirements, back-of-envelope capacity estimation, a high-level design diagram, a deeper deployment architecture diagram, sequence diagrams for authenticated routing and for rate limiting, and an entity-relationship diagram for routes, API keys, and policies - with the reasoning an interviewer expects behind every box and arrow.

~200K/sPeak requests routed
<5msAdded p99 gateway latency
<10sService registry propagation
API clientGET /v1/orders Gatewayauth + limit + route Token bucketper API key Orders servicebackend, discovered 429 responseif over limit response streamed back

1. Clarify requirements before drawing any box

An API gateway is a single choke point that every request passes through, which means its design is dominated by one constraint: it must add negligible latency and never become the thing that goes down, even while enforcing auth, quotas, and routing on every single call.

Functional requirements

Route requestsMap an incoming path/host to the correct backend service, resolved dynamically via service discovery.
Authenticate & authorizeValidate API keys/JWTs and enforce which routes a given identity is allowed to call.
Rate limitEnforce per-key and per-route request quotas, rejecting excess traffic before it reaches a backend.
Observe & controlCentralized logging, metrics, and the ability to update routing/policy without redeploying backends.

Non-functional requirements

Minimal added latencyThe gateway itself should add well under 5ms p99 on top of the backend's own response time.
High availabilityThe gateway is a single point of failure by construction; it must be deployed with no single instance of failure.
Fast policy propagationA new route, revoked API key, or updated rate limit should take effect across all gateway nodes within seconds, not minutes.
Graceful backend degradationA slow or failing backend must not exhaust the gateway's own connection pool and take down unrelated routes.
Explicitly out of scope Full API management developer-portal UX (self-serve key signup, billing), a GraphQL federation layer, and a service mesh's sidecar-per-pod model are called out as extensions/alternatives rather than core requirements, so the core design stays focused on routing, auth, and rate limiting.

2. Back-of-the-envelope capacity estimation

These numbers decide whether rate-limit counters can live in a single Redis instance, how large the connection pool to each backend needs to be, and how quickly route/policy changes must propagate to avoid stale enforcement.

MetricAssumptionResulting estimate
Peak requests routedA mid-size platform fronting ~40 backend services~200,000 requests/sec peak across all routes combined
Rate-limit checksEvery request checks a token bucket200,000 checks/sec - must be an in-memory/Redis counter operation, never a relational row-level lock
Distinct API keys / policies~500,000 active API keys, ~200 distinct route policiesSmall enough that the full route table and policy set fit comfortably in each gateway node's local memory cache
Auth token validationJWT signature verification is CPU-bound, not I/O-bound~200,000 verifications/sec spread across a horizontally scaled gateway fleet - stateless, no database round-trip per request
Config/policy change rateRoute and policy updates happen ~dozens of times/day, not per-requestLow write volume; the win is in propagation speed (push, not poll) rather than write throughput
Why this matters Every one of the 200,000 requests/sec must be authenticated and rate-limit-checked, but the underlying route/policy/key data changes only dozens of times a day. That asymmetry is the number that justifies caching all routing and policy data in-memory on every gateway node and pushing invalidations, rather than querying a central database on the hot path.

3. High-level design (HLD)

The HLD treats the gateway as a request pipeline of independent, ordered stages - auth, rate limit, route resolution, proxy - each backed by its own fast local cache, with a separate control plane responsible for pushing configuration changes to every node.

ClientAPI request Auth stageJWT / API key Rate-limit stagetoken bucket Route resolverpath → service Backend servicevia discovery API keys / JWKS cache Counter store (Redis) Service registry Control plane / admin APIroutes, keys, policies (source of truth)
Request pipeline stagesLocal caches / fast infraBackend servicesControl plane

What each box owns

Auth stage

Validates the caller's identity - either a signed JWT (verified against a locally cached JWKS public key set, no network call per request) or an API key (looked up in a local in-memory cache refreshed from the control plane). A request that fails auth is rejected here, before it ever touches rate limiting or routing, saving downstream work.

Rate-limit stage

Checks the caller's token bucket for the specific route being called. The bucket state lives in a shared, fast counter store (Redis, using atomic INCR/Lua scripts) so that all gateway nodes enforce the same limit consistently for a given key, rather than each node keeping an independent (and therefore gameable) local counter.

Route resolver & service registry

Matches the request path/host against the cached route table to determine the target backend service, then resolves that service name to a live, healthy instance via the service registry (Consul/etcd/Kubernetes DNS) - never a hardcoded IP. This is what lets backend services scale up/down or move without any gateway configuration change.

Control plane / admin API

The single source of truth for routes, API keys, and policies, exposed via an internal admin API used by platform engineers (or a CI/CD pipeline) to publish changes. It never sits on the request hot path itself - instead it pushes invalidations/updates to every gateway node's local cache, which is what keeps per-request latency low while still letting configuration change in near real time.

4. Detailed architecture diagram

The architecture diagram shows the gateway deployed as a stateless, horizontally scaled fleet behind a load balancer, a shared rate-limit counter store, and a control plane that pushes config changes via pub/sub instead of every node polling a database.

Edge layer GeoDNS / Anycast L4/L7 load balancer TLS termination DDoS scrubbing Gateway fleet (stateless, autoscaled) Gateway node ×80 pods Local route/policy cache Circuit breaker per backend Connection pool per backend Shared fast-path infra Redis cluster (rate-limit counters) Service registry (etcd/Consul) JWKS cache (public keys for JWT verification) Control plane Admin API ×4 pods Config DB (source of truth) publishes change events over pub/sub to every gateway node Backend services (behind the gateway) Orders svc Payments svc Inventory svc each registers itself with the service registry on startup/shutdown
DecisionChoiceReasoning
Rate-limit stateShared Redis cluster, not per-node local countersA client hitting different gateway nodes across requests must still be limited consistently; local-only counters would let a client multiply its effective quota by the number of nodes.
Route/policy distributionPush (pub/sub) from control plane to local node cache200,000 req/sec cannot afford a database round-trip per request; pushing invalidations keeps reads local while still propagating changes in seconds, not the minutes a polling interval would need.
Per-backend isolationIndependent connection pool + circuit breaker per backendA slow or failing "Payments" backend must not exhaust the shared thread/connection pool and start failing unrelated "Inventory" requests too.
Service discoveryDynamic registry (etcd/Consul/K8s DNS), not static configBackends scale, restart, and get rescheduled constantly; hardcoded IPs would require a gateway config change (and redeploy) for every backend topology change.

5. Sequence diagrams for the two critical flows

Authenticated routing shows the full happy-path pipeline order; rate limiting shows exactly how a shared counter store makes throttling consistent across a horizontally scaled gateway fleet.

5.1 Authenticated request routing to a backend service

Client Gateway Registry Orders svc 1. GET /v1/orders/42, Bearer JWT 2. verify JWT sig against cached JWKS (local, no I/O) 3. check token bucket for this key (local + Redis) 4. resolve route: /v1/orders/* → orders-service 5. any healthy instance? 6. instance @ 10.0.4.12:8080 7. proxy request, add X-Request-Id 8. stream response back

Steps 2-4 are all local, in-memory operations on the gateway node - no network call, no database query - which is exactly how the pipeline stays under a few milliseconds of added latency. Step 5 is the only external lookup in the fast path, and it's typically served from the registry's own local cache on the gateway node too, refreshed on a short interval or via watch/subscribe rather than queried synchronously per request in practice.

5.2 Rate limiting with a shared token bucket

Client Gateway node A Redis counter Gateway node B 1. request #1, api_key=abc 2. EVAL token-bucket script (atomic) 3. allowed, 99 tokens left 4. 200 OK 5. request #2 hits a different node, same key 6. EVAL token-bucket script (same key) 7. quota exhausted → denied 8. 429 Too Many Requests, Retry-After

Step 2 uses an atomic Lua script in Redis rather than a separate GET-then-SET, so two concurrent requests for the same key from different gateway nodes can never both read the same token count and both decide they're allowed - the check-and-decrement is a single atomic operation server-side. This is precisely why a shared counter store (not per-node local counters) is required in step 5-7: node B correctly denies the request because it consults the same shared state node A wrote to, even though B has never seen this client before.

6. Entity-relationship (ER) diagram and schema

The schema has to answer: how is an incoming path matched to exactly one route, how is an API key's identity and quota looked up in constant time, and how are rate-limit/auth policies attached to a route without duplicating them per route.

routes PK id BIGINT path_pattern VARCHAR method ENUM target_service VARCHAR FK policy_id BIGINT requires_auth BOOLEAN updated_at TIMESTAMP policies PK id BIGINT name VARCHAR rate_limit_rps INT burst_size INT allowed_roles JSON timeout_ms INT retry_count INT api_keys PK id BIGINT UQ key_hash CHAR(64) FK policy_id BIGINT owner VARCHAR status ENUM created_at TIMESTAMP N1 1N many routes reference one shared policy; one policy can also be attached directly to many api_keys (per-caller override)

Key modeling decisions

policies is shared, not duplicated per routeDozens of routes commonly share the same rate limit/timeout profile ("public read-only tier"); a shared policy table lets one update change all of them atomically.
api_keys stores key_hash, never the raw keyOnly a salted hash of the API key is persisted, so a database leak doesn't directly expose usable credentials - the gateway hashes the incoming key and compares.
routes.path_pattern supports wildcardsA single row like /v1/orders/* covers an entire resource family instead of one row per exact endpoint, keeping the route table small enough to cache fully in memory.
policy_id on both routes and api_keysA route's policy sets the default; an api_key's own policy_id, when present, overrides it - giving a specific high-volume partner a higher limit without a special-case in the route table.
Storage choiceUse whenWatch out for
Relational (Postgres) as the control-plane source of truthStrong consistency matters for who-can-call-what, and the write volume is low (config changes, not requests).Never queried on the request hot path directly - always read from each gateway node's local cache.
In-memory (Redis) for rate-limit countersAccess pattern is atomic increment/check at extreme request volume, shared across a fleet.Counter state is intentionally ephemeral; a Redis failover briefly resets in-flight windows, which is an accepted trade-off for the speed gained.

7. Deep dives interviewers actually probe

Token bucket vs sliding window vs fixed window - which rate-limiting algorithm and why?

A fixed window (e.g. "100 requests per calendar minute") is simple but allows a burst of 200 requests right across a window boundary (100 at 0:59, 100 at 1:00). A sliding-window log is precise but requires storing a timestamp per request, which is memory-expensive at scale. A token bucket is the common middle ground: it allows controlled bursting up to the bucket size while enforcing a steady average refill rate, with O(1) state per key (just a count and a last-refill timestamp) - the reasoning above uses it for exactly that memory-efficiency and burst-tolerance balance.

-- Redis Lua script: atomic token-bucket check (simplified)
local tokens = tonumber(redis.call('GET', KEYS[1]) or capacity)
local elapsed = now - last_refill
tokens = math.min(capacity, tokens + elapsed * refill_rate)
if tokens < 1 then return 0 end
redis.call('SET', KEYS[1], tokens - 1)
return 1

What happens when a backend service is slow or down?

Each backend gets its own connection pool and circuit breaker. Once error rates or latency for a specific backend cross a threshold, the circuit opens: the gateway immediately fails fast (returns 503) for that backend's routes instead of piling up slow, doomed requests and exhausting shared resources - and periodically sends a small number of trial requests to detect recovery (half-open state) before fully reopening the circuit.

How does the gateway avoid becoming a single point of failure itself?

The gateway is deployed as a stateless, horizontally scaled fleet behind a load balancer across multiple availability zones - any single node can fail without dropping traffic. The genuinely stateful dependencies it leans on (rate-limit counters, service registry) are themselves deployed as clustered, replicated systems, and the gateway is built to degrade gracefully if one of them is briefly unavailable - e.g., falling back to a slightly stale local route cache rather than rejecting all traffic if the control plane is unreachable.

JWT vs opaque API key - why support both, and how does verification differ?

A JWT is self-contained and verifiable locally (signature check against a cached public key) with zero network calls, ideal for high-volume, short-lived user sessions, but it can't be instantly revoked before its expiry without an extra denylist check. An opaque API key requires a lookup (typically against a local cache refreshed from the control plane) but can be revoked instantly by simply removing it, which is why long-lived machine-to-machine credentials are usually modeled as API keys rather than JWTs.

What is the single biggest bottleneck as this scales 10x?

Not the gateway compute itself - it's stateless and scales linearly by adding nodes. The real bottleneck becomes the shared rate-limit counter store: at 2 million checks/sec, a single Redis cluster's network and CPU capacity for atomic Lua-script executions becomes the constraint. The fix is sharding the counter keyspace across multiple Redis clusters (e.g. by hash of api_key) rather than scaling a single cluster indefinitely, accepting slightly more complex client-side routing logic in exchange.

8. Summary: what a strong answer covers

Ordered the pipeline: auth → rate limit → routeJustified every number with a calculationChose token bucket and explained the alternatives Made the gateway itself stateless and horizontally scaledIsolated each backend with its own pool and circuit breakerPushed config changes instead of polling on the hot path
Interview tip When asked to design an API gateway, the strongest signal is treating "the gateway must add near-zero latency" as the governing constraint on every other decision - caching routes, keys, and policies locally on each node and pushing changes rather than reading a database per request is the pattern that makes 200,000 requests/sec with sub-5ms overhead possible at all.
No comments
Leave a Comment