What belongs in the gateway and what does not
The gateway's job is everything true for every request regardless of which service handles it: terminating TLS, authenticating the caller, enforcing rate limits, routing, and emitting a correlation ID. Those concerns are genuinely cross-cutting, and implementing them once at the edge is what lets the services behind it stay simple.
Business logic does not belong there. The most damaging anti-pattern I encounter is the gateway that grew rules — this customer tier gets that discount, this region skips that validation. It becomes a shared mutable component every team must change and nobody owns, and because it sits in the path of every request, a mistake in it is a total outage rather than a partial one.
Aggregation, and why backend-for-frontend usually wins
A mobile client needing six calls to render one screen pays for six round trips on a high-latency network. Aggregation at the edge collapses that into one request, and the saving is real. The trap is a single aggregation layer serving every client type, because a web dashboard and a mobile app want different shapes, and a shared aggregator ends up serving the union of both — over-fetching for everyone.
A backend-for-frontend per client type avoids that. Each is owned by the team that owns the client, shaped exactly for its screens, and free to change at that client's release cadence. It is more components, but each is small with a single consumer, which is far easier to evolve than a shared layer with three masters.
Rate limiting that reflects who is calling
A single global limit protects the platform and punishes your best customers. Limits should be tiered by identity — per API key, per customer plan, per endpoint cost. An endpoint that triggers report generation is not equivalent to one returning a cached lookup, and a limit treating them the same is either too loose for one or too tight for the other.
Return the standard rate-limit headers so clients can back off intelligently rather than hammering and guessing. And distinguish 429 from 503 honestly: the first says you exceeded your allowance, the second says we are struggling. Clients react differently to each, and conflating them produces retry behaviour that makes an overload worse.
Circuit breaking, timeouts and graceful degradation
Every downstream call needs a timeout shorter than the client's patience and a circuit breaker that stops sending traffic to a service that is clearly failing. Without a breaker, one slow dependency consumes the gateway's connection pool and takes down routes that have nothing to do with it — the classic cascading failure.
Degradation should be designed rather than accidental. If the recommendations service is down, the product page should render without recommendations, not return a 500. Deciding in advance which dependencies are essential and which are decorative is an architectural exercise, and doing it is what keeps a partial outage partial.
Versioning you can actually retire
Put the major version in the path. Header-based negotiation is more elegant and harder to debug, cache, and explain to a client integrating at 2am. Within a major version, add fields freely and never change the meaning of an existing one — that additive discipline is what keeps consumers working without coordinated releases.
The part teams skip is retirement. A version with no sunset plan is permanent, and permanent versions are why gateways accumulate a decade of routing rules. Track usage per version per consumer, publish a deprecation timeline, and contact the remaining callers directly. Without usage data you can never turn anything off, because nobody can prove it is safe.
Key takeaways
- Keep only truly cross-cutting concerns at the edge; business rules in the gateway become an unowned shared component
- Prefer a backend-for-frontend per client type over one shared aggregation layer serving everyone badly
- Authenticate at the gateway, authorise in the service — only the service has the domain state to decide
- Tier rate limits by identity and endpoint cost, and return standard headers so clients back off properly
- Give every downstream call a timeout and circuit breaker, and decide in advance which dependencies are optional
- Version in the path, add fields additively, and track per-consumer usage so versions can actually be retired
Conclusion
A good gateway is boring: it authenticates, routes, limits, and stays out of the way. The discipline is resisting the pull to put just one more piece of logic at the edge, because that is how a routing layer turns into the most fragile component in the system.
Enjoyed this article?

Vivek Kumar Singh
Technical Expert · Full Stack Cloud Engineer · Tokyo, Japan