Skip to content

API Gateway Patterns โ€‹

๐Ÿ“ Context โ€‹

The customer needs a strategy for exposing services to consumers โ€” internal teams, external partners, or public developers. An API gateway sits at the boundary between consumers and backend services, handling cross-cutting concerns like authentication, rate limiting, routing, and observability so individual services don't have to.

๐Ÿ“‹ Decision Checklist: Does This Need an API Gateway? โ€‹

  • [ ] Multiple backend services behind a single consumer-facing API
  • [ ] Cross-cutting concerns (auth, rate limiting, logging) duplicated across services
  • [ ] Need to present a stable API surface while backends evolve
  • [ ] Multiple consumer types (web, mobile, partner, internal) with different API needs
  • [ ] API versioning, deprecation, or lifecycle management required

If this is a single service with one consumer type: A gateway adds unnecessary infrastructure. Handle auth and rate limiting in the service directly.

Say it like this

"A gateway earns its place when you have several services behind one front door, or several kinds of consumers hitting them. If it's one service and one client, putting a gateway in front is just another hop to operate and pay for โ€” I'd handle auth and limits in the service."

%%{init: {'theme': 'neutral', 'themeVariables': {'fontSize': '14px'}}}%%
flowchart LR
    Web[Web app] --> Gateway[API gateway]
    Mobile[Mobile app] --> Gateway
    Partner[Partner API] --> Gateway

    Gateway --> Auth[Auth and rate limits]
    Gateway --> Users[User service]
    Gateway --> Orders[Order service]
    Gateway --> Products[Product service]
    Gateway --> Logs[Access logs and metrics]

๐Ÿงฉ Worked Scenario: One Screen, One Call โ€‹

A mobile app opens the order-confirmation screen. That screen needs three things โ€” the order itself, the product details for each line item, and the customer's loyalty status. Behind the gateway those are three internal services (Order, Product, Loyalty). Instead of the app making three authenticated calls, it makes one call to a gateway endpoint shaped for this screen (a BFF route).

API gateway request lifecycle: one client request enters the gateway, which terminates TLS, authenticates, rate-limits, then routes and aggregates in parallel to the Order, Product, and Loyalty services before composing a single 200 response.
One client request, fanned out behind a single secure front door, composed into one response.
1 ยท Terminate & authenticate
Gateway ends TLS, validates the JWT, and extracts identity. An unauthenticated request never reaches a service.
2 ยท Throttle
Checks the caller against its rate limit. Over the limit returns 429 with Retry-After โ€” the backends are never touched.
3 ยท Route & aggregate
Fans out to Order, Product, and Loyalty in parallel and composes one payload shaped for this screen.
4 ยท Cache & log
Caches the idempotent product lookup, emits one access log with latency and the upstreams hit, then returns 200.

Why this beats three direct calls: the app makes one round trip instead of three, the three services never implement auth or rate limiting themselves, and when the screen later needs a fourth field, the BFF route changes โ€” not the app.

Say it like this

"The app makes one call to a route shaped for that screen. The gateway proves who you are, checks you're under your limit, fans out to the three services in parallel, and hands back a single payload. My services never see an unauthenticated request and never write rate-limiting code."

๐ŸŽฏ Core Patterns โ€‹

Gateway Routing โ€‹

The gateway routes requests to the appropriate backend service based on path, headers, or other request attributes.

Consumer โ†’ Gateway โ†’ /users/* โ†’ User Service
                   โ†’ /orders/* โ†’ Order Service
                   โ†’ /products/* โ†’ Product Service

Benefits: Single entry point, simplified consumer configuration, backend services can be moved or scaled independently.

Risks: Gateway becomes a single point of failure. Ensure high availability (multi-AZ, auto-scaling).

Gateway Aggregation โ€‹

The gateway composes responses from multiple backend services into a single response for the consumer. Useful when a single screen or operation requires data from multiple services.

When to use:

  • Mobile clients with latency sensitivity (one call better than five)
  • Composing a view that spans multiple domains
  • Reducing chattiness between consumer and backend

When to avoid:

  • Complex business logic in the aggregation โ€” that belongs in a service
  • Aggregation requires transactional consistency โ€” gateway can't manage transactions

Backend for Frontend (BFF) โ€‹

Separate gateway instances (or configurations) for different consumer types. Each BFF is tailored to its consumer's needs.

Web App โ†’ Web BFF โ†’ Backend Services
Mobile App โ†’ Mobile BFF โ†’ Backend Services
Partner API โ†’ Partner BFF โ†’ Backend Services

Why: Web, mobile, and partner APIs have different payload requirements, authentication flows, and rate limits. A single gateway trying to serve all of them becomes bloated.

Tradeoff: Multiple gateways to maintain. Justified when consumer needs genuinely diverge.

Say it like this

"I reach for a BFF when the web, mobile, and partner clients start pulling the same endpoint in three directions. Rather than bloat one gateway with conditional payloads, I give each consumer a thin tailored layer over the same backends."

Gateway Offloading โ€‹

Move cross-cutting concerns from individual services to the gateway:

ConcernGateway HandlesService Handles
AuthenticationToken validation, identity extractionAuthorization (what this user can do)
Rate limitingPer-consumer or per-API-key throttlingBusiness-level quotas if needed
TLS terminationExternal TLS, certificate managementInternal mTLS (service mesh) if required
Request loggingAccess logs, latency metricsBusiness event logging
CORSPreflight handling, header managementNothing (gateway handles it)
Request/response transformationHeader injection, format conversionBusiness logic transformation
CachingResponse caching for idempotent GETsCache invalidation signals

API Versioning โ€‹

How to evolve APIs without breaking existing consumers:

StrategyMechanismTradeoff
URI versioning/v1/users, /v2/usersExplicit, easy to route. Clutters URI space.
Header versioningAccept: application/vnd.api.v2+jsonClean URIs. Less discoverable, harder to test.
Query parameter/users?version=2Simple. Looks like a filter, not a version.
No versioning (additive only)New fields added, old fields never removedSimplest. Only works if you never break compatibility.

Recommendation: URI versioning for external/partner APIs (explicitness wins). Header versioning or additive-only for internal APIs (less ceremony).

Say it like this

"For anything a partner consumes, I version in the URI โ€” /v2/orders โ€” because it's explicit and trivial to route. Internally, where I control both ends, I stay additive: new fields are fine, removing one is a breaking change I version for."

๐Ÿ›ก๏ธ Reliability & Failure Handling โ€‹

A gateway is the one place that sees every request, so it's where you protect consumers from backend failures and backends from abusive load. This is the layer that separates a demo from production.

%%{init: {'theme': 'neutral', 'themeVariables': {'fontSize': '14px'}}}%%
flowchart LR
    G[API Gateway] --> H{Backend healthy?}
    H -->|No| E503[503 Service Unavailable]
    H -->|Yes| F[Forward request]
    F --> R{Response in time?}
    R -->|2xx| OK[Return 200]
    R -->|Timeout| E504[504 Gateway Timeout]
    R -->|5xx| CB{Circuit breaker open?}
    CB -->|Yes| E503
    CB -->|No| RT[Retry once or fail fast]

Rate limiting โ€” with the numbers stated โ€‹

Vague advice ("add rate limiting") invites the follow-up you can't answer. State the policy. Use a token-bucket so short bursts are allowed but sustained abuse is throttled. Limits below are illustrative โ€” set real ones from observed traffic, not a guess.

CallerLimit (illustrative)Over-limit response
Unauthenticated (per IP)~60 req/min429 + Retry-After
Authenticated (per API key)~1,000 req/min429 + Retry-After
Bursttoken bucket refills steadilythrottle when bucket is empty

Status codes the gateway returns โ€‹

The gateway should fail in ways the consumer can act on โ€” reject early, and never hang.

ConditionGateway returnsReasoning
Missing / invalid token401Reject before any backend is touched
Authenticated but not permitted403Coarse checks at gateway; fine-grained authz in the service
Over rate limit429Return Retry-After so the client backs off correctly
Backend exceeded timeout504AWS API Gateway's default integration timeout is 29s (verified)
Backend unhealthy / circuit open503Health check pulled it from the pool; fail fast, don't pile on
Backend returned 2xx200Pass through, log latency and upstream
API gateway response outcomes: the gateway maps each condition to a status the consumer can act on โ€” 200 success, 401 invalid token, 429 over limit, 503 circuit open, 504 backend timeout.
Fail in ways the consumer can act on โ€” reject early, never hang.
  • Health checks: the gateway only routes to instances passing health checks โ€” a dead backend is removed from rotation instead of returning errors to consumers.
  • Circuit breaker: after N consecutive failures (illustrative, e.g. 5), the breaker opens and the gateway fails fast with 503 instead of stacking requests on a struggling service. A half-open probe after a cooldown tests recovery.
  • Timeouts: every hop has a budget. A consumer should get a clean 504 quickly, not a 30-second hang while the gateway waits on a dead backend.
Say it like this

"The gateway protects the backends from each other. If the product service starts timing out, the circuit breaker opens and I return a fast 503 instead of stacking calls on a dying service โ€” and a health check pulls the bad instance out of rotation. The consumer gets a clean status with a Retry-After, never a 30-second hang."

๐Ÿ‘๏ธ Observability โ€” Who Sees What โ€‹

The gateway is the natural choke point for telemetry โ€” every request passes through it. Decide deliberately what each audience sees.

AudienceWhat they seeWhy it matters
EngineeringPer-route p99 latency, error rates, upstream health, full access logs + tracesPinpoint the slow hop without touching production
Customer SuccessPer-consumer request volume, error summary, which key is being throttledAnswer "why are my calls failing" without pulling in engineers
Partner / consumer devX-RateLimit-Remaining headers, 429 + Retry-After, status page, developer portalSelf-serve โ€” they know their limits before they hit them
Alerting5xx rate / p99 latency threshold โ†’ Datadog / PagerDuty / SlackCatch degradation before consumers report it
Say it like this

"Every request gets one access log with its latency and the upstream it hit, so when someone says 'the API is slow' I point at the exact route and backend. Partners see their remaining quota in the response headers, so a 429 is never a surprise."

๐ŸŽฏ Technology Selection โ€‹

TechnologyTypeBest ForConsiderations
AWS API GatewayManagedREST and WebSocket APIs on AWSPer-request pricing, 29-sec timeout, deep AWS integration
KongSelf-managed or cloudPlugin-rich, multi-cloudLua-based plugins, active community, operational overhead if self-managed
EnvoyProxy / Service meshHigh-performance, L4/L7, gRPC-nativeOften paired with Istio, steep learning curve
NGINXProxySimple routing, TLS termination, static configLightweight, proven. Limited dynamic routing without Plus.
Azure API ManagementManagedAzure-native APIs, developer portalPolicy-based transformation, built-in developer portal
Google ApigeeManagedEnterprise API management, monetizationFull API lifecycle management, complex feature set
TraefikProxyKubernetes-native, auto-discoveryIntegrates with K8s ingress, good for container environments

Selection Criteria โ€‹

  • Managed vs. self-managed: Managed reduces operational burden but limits customization
  • Protocol support: REST-only, or do you need gRPC, WebSocket, GraphQL?
  • Plugin ecosystem: Can you extend it for custom auth, transformation, or validation?
  • Performance: What's the latency overhead? (typically 1-5ms for well-configured gateways)
  • Multi-environment: Can it run consistently across dev, staging, production?

๐ŸŽฏ API Design Principles for SA Engagements โ€‹

When reviewing or designing APIs at the gateway layer:

  • Consistency over cleverness โ€” consistent naming, error formats, pagination across all APIs
  • Consumer-first design โ€” design the API the consumer wants to call, then figure out the backend routing
  • Versioning strategy decided upfront โ€” retrofitting versioning is painful
  • Documentation as code โ€” OpenAPI specs generated from code, not maintained separately
  • Rate limiting with clear communication โ€” return 429 with Retry-After header, document limits in developer portal

โš ๏ธ Gotchas โ€‹

  • Gateway as the place for business logic โ€” routing and cross-cutting concerns only
  • Single gateway for all consumer types โ€” BFF pattern exists for a reason
  • No health checks on backends โ€” gateway routes to a dead service, consumers get errors
  • TLS termination at gateway without internal encryption โ€” data is unencrypted inside the network
  • Rate limits too aggressive for legitimate use โ€” instrument first, then set limits based on real usage
  • Gateway configuration drift between environments โ€” treat gateway config as code, deploy through CI/CD
  • Ignoring gateway latency in SLA calculations โ€” every hop adds latency

๐Ÿ“š Further reading โ€‹

Gateway patterns and the security standard that applies to them:

Built as a public field guide for practical Solutions Engineering and Architecture work.