gw-03 — Analysis
A design-review treatment of the filter-chain gateway you build in
steps/.
Required behaviors
- Phase ordering is enforced, not incidental. Inbound filters all
run (in
order) before the endpoint filter; outbound filters all run after. The framework, not the filter author, guarantees this. - Short-circuit is first-class. Any inbound filter can set the response and stop the chain (401, 429, cache hit). Once stopped, no further inbound or endpoint filters run, but outbound filters still run (so the access log and metrics always fire).
- Errors are handled in-band. A filter that throws is converted to a 5xx by an error filter; the chain still produces a response and an access log. A gateway that can crash a connection on a filter bug is not production-grade.
- Never block the loop. Endpoint origin calls are async with a
timeout; in Go this is a goroutine +
contextdeadline, in Java/Netty aCompletableFuturewith a timeout. - Hop-by-hop hygiene. Strip
Connection,Keep-Alive,Proxy-*,TE,Transfer-Encoding,Upgradebefore forwarding (RFC 9110 §7.6.1); add/normalizeX-Forwarded-For/Forwarded.
Design decisions
-
One mutable
RequestContextthreaded through the chain. Mirrors Zuul'sSessionContext. Simpler than passing many args, and matches the real codebase so the lab transfers. The cost is shared mutable state — filters must document what attributes they read/write. -
Filters are interfaces, not closures.
Type(),Order(),ShouldFilter(),Apply(). The explicitOrder()andType()make ordering reviewable and testable; closures would hide it. -
Endpoint filter returns the response (async). The endpoint phase is the only one that produces a response from an origin; inbound filters only decide and decorate. This keeps "where the origin call happens" in exactly one place — where pooling (gw-04) and resilience (gw-06) plug in.
-
Routing is data. The route table is a struct you can replace atomically (a pointer swap under an
atomic.Value/RWMutex), which previews the control-plane hot-reload of gw-08. No redeploy to change a route.
Tradeoffs worth flagging
-
Centralization = leverage + blast radius. Every filter runs on every request; a 1ms regression in an inbound filter is 1ms on 1M+ rps. The review bar for gateway code is higher than for a single service. This is why the role lists "high-quality code reviews" and "detailed design reviews" as core duties.
-
Sync vs async filters. Zuul distinguishes
HttpInboundSyncFilter(cheap, CPU-only, runs inline) from async filters (may do I/O). Make the cheap path cheap; only pay the async machinery when a filter actually needs I/O. Misclassifying a blocking filter as sync is the classic event-loop-stall bug. -
Dynamic vs typed filters. Netflix historically shipped Groovy filters loadable at runtime (fast iteration, no deploy) and learned the operational cost (a bad dynamic filter, fleet-wide, instantly). Modern practice leans toward typed, reviewed, deployed filters with config-driven behavior. Be able to argue both sides.
-
State on a "stateless" gateway. Rate-limiter buckets, circuit state, and pools are per-instance state. That means a token-bucket limit is really per-instance × instances unless you use a shared store — a subtle correctness point for distributed rate limiting (gw-06).
What production adds beyond this lab
- Request/response body buffering vs streaming decisions (buffer to inspect/transform, stream to preserve latency and bound memory).
- A WAF / security filter stage and bot management (gw-07).
- Per-route timeouts, retries, and circuit-breaker config from the control plane (gw-06, gw-08).
- Backpressure into the filter chain so an overloaded gateway sheds load at admission rather than collapsing (gw-06).
- Full observability: per-filter latency, per-route RED metrics, trace spans that cross the proxy (gw-11).
- Safe rollout machinery for filters: flags, shadow, canary, auto-rollback (gw-12).