Handler order is load-bearing in a Chain of Responsibility. What failures does wrong ordering cause, and how do you make order explicit, testable, and safe to extend?
answer
- shadowing: general before specific = dead handler
- prerequisite inversion: authz before authn
- one owner for order; phases > magic numbers
- disjoint guards make order irrelevant
- golden-order test + log chain at startup
basics
~20 sWrong order can let a broad handler swallow requests meant for a specific one, or let a step run before its prerequisite (e.g. authorization before authentication). Fix it by owning order in one explicit, tested place instead of leaving it implicit.
solid answer
~50 sTwo distinct hazards. **Shadowing**: in a first-match chain, a broad handler placed before a specific one consumes requests the specific handler was written for, leaving it dead and untested. **Prerequisite inversion**: a handler runs before the stage that establishes its precondition — authorization before authentication, business logic before rate limiting, caching before request normalization — producing wrong results or security holes. Both are invisible to the type system because the chain is just a list. Mitigations: keep ordering in exactly one place (an explicit builder, an ordered configuration list, or `@Order`-style priority metadata) rather than scattered `next` assignments; prefer coarse named phases (auth → validate → enrich → dispatch → fallback) so a new handler declares a phase, not a magic integer; make guards mutually exclusive where possible so order stops mattering; assert the resolved order in a test that fails when a new handler is registered; and emit the chain's composition at startup and the traversal path per request so production shows who actually handled it.
code
pseudocode · 11 lines// order owned in ONE place, expressed as phases, and asserted in a test
val chain = ChainBuilder()
.phase(TRACE) { CorrelationId() }
.phase(AUTHN) { JwtAuthentication() }
.phase(AUTHZ) { RoleCheck() } // may assume principal exists
.phase(VALIDATE) { SchemaValidation() }
.phase(DISPATCH) { InvoiceHandler(); GenericJsonHandler() } // specific first
.phase(FALLBACK) { NotFoundHandler() }
.build()
// test: assert(chain.names() == listOf("CorrelationId","JwtAuthentication", ...))go deeper
Know that a chain runs top to bottom and that a handler which matches everything must go last, or handlers after it never run.
Name shadowing and prerequisite inversion with concrete examples, and put ordering in one explicit builder or config list rather than in scattered next-pointers.
Add phase-based ordering, disjoint guards or map dispatch to remove the dependency, golden-order and end-to-end shadowing tests, and per-handler metrics that expose dead links.
Treat the order as a published contract for an extension point: define phases and what each may assume, enforce with declared constraints plus startup topological sort and fail-fast, and require traversal-path telemetry so production can answer 'who handled this' without a debugger.
## Why order is a real dependency A Chain of Responsibility turns a conditional cascade into a list of objects. The `else if` sequence was at least visible in one function; the list's order is often produced by classpath scanning, DI registration, or a config file, and no compiler checks it. Order is therefore a **hidden coupling between handlers that never reference each other**. ## Failure mode 1 — shadowing (first-match chains) ``` handlers = [ AnyJsonRequestHandler, InvoiceJsonRequestHandler ] ``` The general handler matches every JSON request, consumes it, and the invoice handler never runs. Symptoms: the specific behavior "mysteriously stops working" after an unrelated deployment; unit tests for the specific handler pass (it is tested in isolation) while the system is wrong. This is the same rule as exception `catch` clause ordering, routing tables, and firewall rules: **most specific first**. Detection: coverage or a counter per handler — a handler that never fires in production is either dead or shadowed. ## Failure mode 2 — prerequisite inversion Some stages establish state that later stages assume: - authentication resolves the principal → authorization needs it - request normalization/decoding → validation and caching need canonical input - rate limiting / quota → must precede expensive work to be useful - tracing/correlation-id → must be first for downstream logs to be attributable - transaction or tenancy context → must wrap the handlers that touch storage Getting these backwards ranges from wasteful (rate limiting after the expensive call) to a security defect (authorization evaluated against an unresolved principal, or a cache keyed on un-normalized input serving one user's data to another). ## Failure mode 3 — ordering that changes results silently In broadcast/enrichment chains everyone runs, so nothing looks broken, but the composed outcome depends on order: two enrichers writing the same field, a sanitizer running after a formatter, a metric recorded before a retry. These are the hardest to catch because there is no missing behavior, only a wrong value. ## Making order explicit 1. **Single owner.** One factory/builder/config that lists the chain top to bottom, read like a script. Scattering `a.next = b` across constructors is the worst case — order is then a property of object graph assembly nobody reviews. 2. **Named phases over numeric priorities.** `@Order(150)` invites priority-number wars between teams; `phase = AUTHENTICATION` with a fixed, small phase sequence gives coarse guarantees and leaves within-phase order irrelevant by design. Reserve numeric ties for genuinely ordered handlers inside a phase. 3. **Declared constraints.** For large plugin sets, let a handler declare `runsAfter(Authentication)` / `runsBefore(Dispatch)` and topologically sort at startup, failing fast on cycles or unsatisfiable constraints. This turns implicit knowledge into a checkable graph. 4. **Disjoint guards.** If handlers' guards are mutually exclusive by construction (dispatch on an enum, on a URI prefix that cannot overlap, on a sealed request type), order stops affecting correctness. This is the strongest fix: prefer a **lookup map keyed by the discriminator** over a scan when the discriminator is exact — the chain earns its keep only when guards are genuinely overlapping or fuzzy. 5. **Exhaustive dispatch checks.** With a sealed/enumerated request hierarchy you can assert at build time that every case has a handler, removing the fall-through class of bugs entirely. ## Testing order - **Golden-order test**: assert the resolved handler sequence (list of class names) equals an expected list. It fails loudly when someone registers a handler, which is exactly the review trigger you want — the diff shows the new position. - **Shadowing test**: for each specific handler, send a request it should own through the *whole* chain (not the handler alone) and assert that handler responded — instrument via a spy, a returned handler id, or tracing. - **Prerequisite test**: send a request that would misbehave under inversion (unauthenticated request to a protected route; over-quota request that is expensive) and assert the early stage short-circuits. - **Property/fuzz check**: for a first-match chain, generate requests and assert at most one handler consumed, and that no handler is never selected across the corpus. ## Observability - Log the composed chain **once at startup** (ordered class names + phase) — this single line resolves most "why did it behave differently in staging" questions. - Per request, record the traversal path or at least the terminal handler id (a span/attribute, a response header in non-production, a structured log field). Without it, the pattern's runtime indirection becomes an operational blind spot. - Per-handler counters (invocations, matches) reveal dead and shadowed handlers. ## Extension policy When a chain is an extension point for other teams or plugins, order becomes a public contract. Publish it: the phase list, what each phase may assume, whether a handler may consume, and whether it may mutate the request. Otherwise every new plugin author guesses, and the guesses collide.
- Your chain is populated by dependency injection from many modules. How do you stop order from being accidental?Do not rely on discovery order. Require each handler to declare a phase (and optionally runsAfter/runsBefore constraints), topologically sort at startup, and fail fast on cycles or missing constraints. Log the resolved order at boot and pin it with a golden-order test so any new registration shows up as a deliberate diff rather than a silent reshuffle.
- When is a lookup map a better fit than an ordered chain?When the discriminator is exact and handlers are mutually exclusive — dispatch by message type, URL, or enum. A map gives O(1) resolution, no shadowing, no ordering dependency, and can be checked for exhaustiveness. Keep the chain when guards genuinely overlap, are predicate-based, or when several handlers must contribute to the same request.
Firewall rules: a permissive allow all line placed above your carefully written deny rule doesn't error — it just quietly makes the rest of the file decorative.
saying these in an interview costs you the question
- "Order doesn't matter, each handler checks its own condition" — true only if guards are provably disjoint.
- Wiring successors in constructors scattered across modules so nobody can read the resolved order.
- Relying on classpath/DI discovery order, which can change between builds or environments.
- Using numeric @Order priorities across teams, producing priority inflation wars.
- Testing handlers only in isolation, which cannot detect shadowing or prerequisite inversion.