One front door for many services. It removes duplicated cross-cutting work — and quietly becomes the most dangerous single point of failure in your architecture.
Prerequisites: Proxies, Load Balancers Time to read: ~14 minutes
You split your monolith into eight services. Now every one of them needs:
Eight implementations of the same six concerns, in four languages, maintained by five teams, with subtly different bugs. Meanwhile the mobile client now has to know eight hostnames, and a screen that needs data from three services makes three round trips over a cellular network.
An API gateway is where you put the shared concerns.
flowchart LR
M[Mobile] --> G
W[Web] --> G
P[Partner API] --> G
G[API Gateway<br/>auth · rate limit · routing · TLS · logging]
G --> S1[Users service]
G --> S2[Orders service]
G --> S3[Payments service]
G --> S4[Search service]
| Responsibility | What it removes from your services |
|---|---|
| Routing | Path/host → service mapping, in one config instead of client-side knowledge |
| Authentication | Validate the JWT or session once; pass verified identity downstream |
| Rate limiting & quotas | Per-user, per-API-key, per-endpoint → Rate Limiting |
| TLS termination | Certificates managed in one place |
| Request/response transformation | Version translation, field renaming, protocol bridging (REST ↔ gRPC) |
| Aggregation | One client call → several backend calls → one merged response |
| Caching | Serve repeat responses without touching services |
| Observability | Every request logged, traced, and measured in one place |
| API keys & developer portal | Onboarding for third-party consumers |
| Circuit breaking | Contain a failing service before it poisons callers |
🚨 The distinction from a plain reverse proxy is emphasis, not mechanism. Both are proxies. A gateway adds API-specific concerns: identity, quotas, per-consumer policy, aggregation, and documentation. Kong and Ambassador are literally built on Nginx and Envoy.
The gateway is where you should validate identity, because doing it once beats doing it eight times.
1. Client sends: Authorization: Bearer eyJhbGci...
2. Gateway validates the signature, expiry, and audience
3. Gateway forwards to the service with verified identity:
X-User-Id: 4821
X-User-Roles: customer,beta
X-Request-Id: 7f3a...
4. Service trusts those headers — it's on a private network behind the gateway
🚨 And there’s the trap. If a service trusts X-User-Id blindly, and anyone can reach that
service directly — from another pod, a compromised container, a misconfigured security group — they
can impersonate any user by setting a header. This is a genuine, common vulnerability.
Mitigations, in increasing order of rigour:
X-User-* headers before adding its own. Do this always — it’s
one line of config and it closes the obvious hole.Authorization is a different question. Authentication (“who are you?”) belongs at the gateway. Authorization (“may you do this?”) usually belongs in the service, because only the service knows that user 4821 owns order 993. Coarse checks (role-based route access) can live at the gateway; fine- grained ownership checks cannot. → AuthN vs AuthZ
🎙️ “I’d authenticate at the gateway and authorize in the services — the gateway can tell you who the caller is, but only the orders service knows whether this user owns this order.”
A mobile home screen needs the user profile, recent orders, and recommendations. Three services.
Without aggregation: 3 round trips from a phone on a cellular network. At 100 ms each that’s 300 ms of pure latency, three TLS-protected requests, three chances to fail.
With aggregation: 1 client call. The gateway fans out to three services in parallel (in- datacenter, ~1 ms each) and merges the response. 100 ms total.
⚖️ But be careful. Aggregation pulls business logic into the gateway, and the gateway is infrastructure owned by a platform team while the logic belongs to product teams. Do too much of it and you’ve recreated a monolith — one that every team must coordinate to change, with a deployment cadence set by the most cautious stakeholder.
The usual resolution: BFF (Backend for Frontend). Instead of one gateway doing aggregation for everyone, give each client type its own thin aggregation layer, owned by the team that owns that client. The gateway stays generic (auth, rate limiting, routing); the BFFs handle client-specific shaping. → BFF
Or: GraphQL. Let the client specify exactly what it needs and have a GraphQL layer resolve it across services. Solves over-fetching and round trips elegantly; introduces its own problems (N+1 resolvers, caching difficulty, query cost control). → GraphQL
🚨 Everything goes through the gateway. If it’s down, your entire platform is down — not one service, all of them.
This is the central trade-off of the pattern and the thing to raise unprompted in an interview.
How to manage it:
| Concern | Mitigation |
|---|---|
| Availability | Multiple instances across AZs, behind a redundant load balancer. Never one box. |
| It becomes a bottleneck | Keep it stateless and horizontally scalable. Never put per-request state in it. |
| Added latency | It’s a hop: budget 1–5 ms. Fine — unless you’ve stacked three gateways, which happens. |
| Config errors take everything down | Treat gateway config as code: version control, review, staged rollout, fast rollback. |
| Rate-limit state | Use a shared store (Redis) or approximate local counters — don’t make the gateway stateful. |
| Deployment coupling | If every team must change gateway config to ship, you’ve built a bottleneck. Automate route registration. |
⚖️ The organizational risk is as real as the technical one. A gateway owned by one team that every other team must file tickets against becomes the slowest part of your delivery pipeline. Good implementations let services declare their own routes (via annotations, CRDs, or a self-service config repo) so the platform team owns the mechanism, not each individual route.
Do not add a gateway to a monolith. You already have one front door. Adding a gateway in front of a single application buys you nothing and adds a hop, a failure mode, and a thing to operate.
Do not add one for two or three services. A load balancer with path-based routing does 90% of the job. Add the gateway when the duplicated cross-cutting concerns actually hurt.
You may not need one for internal traffic. Service-to-service calls inside a cluster are often better served by a service mesh, which gives you mTLS, retries, and telemetry without routing everything through a central chokepoint. A common mature setup: gateway for north-south (external) traffic, mesh for east-west (internal) traffic.
🎙️ “With three services I’d use path-based routing on the load balancer rather than a gateway — the gateway earns its keep once we have enough services that duplicating auth and rate limiting is a real cost.”
| Tool | Character |
|---|---|
| Kong | Nginx-based, plugin ecosystem, open source + enterprise. The common self-hosted choice. |
| AWS API Gateway | Fully managed, deep Lambda integration. Watch the per-request pricing at scale — it gets expensive fast. |
| Envoy / Contour / Ambassador | Envoy-based, cloud-native, excellent observability, dynamic config. |
| Nginx / HAProxy | Roll your own. Fine if your needs are routing + TLS + basic rate limiting. |
| Apigee, Azure APIM | Enterprise, strong on API products, monetization, and developer portals. |
| Traefik | Auto-discovers services from Docker/Kubernetes labels. Low-friction for smaller setups. |
📐 A cost note worth having: AWS API Gateway is roughly $1–3.50 per million requests. At 10,000 QPS sustained that’s ~26 billion requests/month — well over $25,000/month for the gateway alone. At high volume, self-hosting Envoy or an ALB is dramatically cheaper. Bringing up gateway cost unprompted is a strong senior signal.
| Gain | Cost | |
|---|---|---|
| Central gateway | One place for auth, rate limiting, TLS, observability | Single point of failure; a hop; organizational bottleneck |
| Aggregation at the gateway | Fewer client round trips | Business logic leaks into infrastructure |
| BFF per client | Client-specific shaping, owned by the right team | More services to run |
| Managed gateway | No ops burden | Per-request cost; less control; vendor lock-in |
| Gateway + service mesh | Right tool for north-south and east-west | Two systems to understand and operate |
X-User-* headers, or a caller who reaches a service directly can impersonate anyone.”Run Kong (or Traefik) in Docker in front of two toy services. Configure:
/users → service A, /orders → service B.429 response and its
headers.That third step is the important one: seeing exactly what the gateway injects makes the
“services trust headers” security issue concrete rather than theoretical. Then try calling service A
directly with a forged X-User-Id and confirm it works — that’s the vulnerability you’re defending
against.