skip to content

When is Chain of Responsibility the wrong choice, and what would you use instead — for example a keyed dispatch table, a rules engine, or plain composition?

level: principalimportance: should knowfreq 30%

answer

  1. exact key → map, not chain
  2. small fixed set → plain conditional
  3. all stages always run → pipeline/Decorator
  4. volatile many-rule policy → decision table
  5. chain's real cost = observability debt

basics

~20 s

Avoid it when exactly one receiver is always correct and identifiable — use a lookup map keyed by type. Avoid it when the rules are business policy that changes constantly, when order dependencies are subtle, or when the indirection costs more than the if/else it replaced.

solid answer

~60 s

Chain of Responsibility earns its keep when several handlers could plausibly handle a request, guards genuinely overlap or are predicate-based, and the handler set must change without editing the caller. It is the wrong tool when: the discriminator is exact and mutually exclusive — a **keyed dispatch table** gives O(1) resolution, no shadowing, and build-time exhaustiveness checks; the set is fixed and small — a readable `when`/`switch` beats three files plus wiring; every stage always runs and always transforms — that is a **pipeline/Decorator**, not responsibility selection; the logic is volatile business policy with many-to-many conditions — a **rules engine or decision table** with data-driven rules is more maintainable; ordering constraints are subtle and cross-team — a topologically-ordered phase model or an explicit workflow/state machine expresses the dependencies the chain hides; or the request is a broadcast with no consume semantics — that is **Observer / pub-sub**. The real cost of a chain is operational: control flow only visible at runtime, latent shadowing, and silent fall-through, all of which demand telemetry the simpler alternatives don't need.

go deeper

for a junior

Say that if there is only ever one correct handler and you can find it directly from the request's type, a simple lookup or if/else is clearer than a chain.

for a middle

Give two or three concrete anti-cases — exact key means a map, always-run stages mean a pipeline, broadcast means Observer — and name the shadowing and fall-through risks a chain adds.

for a senior

Structure the decision around whether guards overlap, whether the handler set is open, and what the ordering dependencies are; mention rules engines for volatile policy and the telemetry a chain requires.

for a principal

Frame it as an explicit trade of compile-time checkability for runtime configurability, price in the observability and governance work the chain obliges, and set an organizational rule for when an extension point is warranted versus when a closed, exhaustively-checked dispatcher is the safer platform default.

## The value test Before reaching for the pattern, ask three questions. Chain of Responsibility is justified only when the answers are yes: 1. **Could more than one object legitimately handle this request?** If exactly one always can and you can name it from the request's data, you need *dispatch*, not *responsibility negotiation*. 2. **Do the handlers' guards overlap or involve predicates you can't reduce to a key?** Thresholds, ranges, feature flags, compound conditions — those favor an ordered chain. An exact enum/type/URL favors a map. 3. **Must the handler set change without editing the caller?** Plugins, per-tenant rules, config-driven extension points. If the set is frozen in the same module, indirection buys nothing. ## Anti-case 1 — exact discriminator → keyed dispatch table Scanning handlers to find the one whose `type == "INVOICE"` is a linear search for a hash lookup. A map from discriminator to handler gives: - O(1) resolution regardless of handler count - **no ordering dependency** and therefore no shadowing class of bugs - **exhaustiveness checkable** — assert at startup or in a test that the key set covers the enum/sealed cases - trivially inspectable at runtime (print the map) Keep the chain only if a request can match several keys or if matching is fuzzy (prefix/glob/regex/priority). ## Anti-case 2 — small fixed set → just write the conditional Three branches that live in one module and change together are clearer as one `when`/`switch` than as three classes, an interface, a builder, and a registration. "Replace conditional with polymorphism" is not free: it distributes logic that a reader must now reassemble from several files, and it hides the total behavior no single file states. Introduce the chain when the conditional actually starts changing for reasons outside the module. ## Anti-case 3 — every stage always runs → pipeline / Decorator If no stage may consume and all of them transform, you don't need the "or pass it on" semantics at all. That is a **function pipeline** (`f ∘ g ∘ h`) or **Decorator**. Modelling it as CoR misleads readers into believing stages can short-circuit, and encourages Boolean returns nobody honors. ## Anti-case 4 — volatile many-condition business policy → rules engine / decision table When "which handler" is really "which of 200 business rules applies", encoding each rule as a class produces a chain nobody can reason about, with priorities that fight. Better: a **decision table** (rows of conditions → outcome) or a rules engine, where rules are *data*, can be authored by domain experts, are diffable, and can be checked for overlap and gaps mechanically. The cost is a second execution model to operate; justify it by rule volume and change rate. ## Anti-case 5 — subtle cross-team ordering → explicit workflow / phases A chain encodes ordering as list position, which is exactly the wrong representation when stages have real dependencies ("resolve tenant before quota, quota before pricing"). Prefer declared constraints topologically sorted at startup, a small fixed **phase** model, or — when stages have state and branching — an explicit **state machine / workflow** that names transitions instead of implying them. ## Anti-case 6 — no consume semantics → Observer / pub-sub If every interested party should see the event and none may stop the others, that is broadcast. Using a chain implies ordering and consumption guarantees you don't want, and couples independent subscribers into one sequence where one slow or throwing subscriber affects the rest. ## Anti-case 7 — hot path with a long chain Each request evaluates every guard until a match. Usually irrelevant; occasionally not, when the chain is long, guards are expensive (I/O, regex over large bodies, permission lookups), and the path is hot. Fixes: index/partition handlers by a cheap pre-filter, memoize guard results, or move to a map. Measure before optimizing — the more common problem is comprehension, not CPU. ## The operational bill The cost that surprises teams isn't CPU, it's **observability debt**. A chain moves control flow from source to runtime, so production needs: - startup log of the composed order - per-handler match/invocation counters (finds dead and shadowed handlers) - unhandled counter + alert (finds silent drops) - traversal path or terminal-handler id on traces If you're not willing to fund that, prefer a shape whose behavior is readable from the source. ## Sound bite for an interview "Chain of Responsibility trades compile-time explicitness for runtime configurability. Take that trade when the handler set is genuinely open and guards genuinely overlap, and pay for it with ordering governance and telemetry. When the receiver is determined by an exact key, or the set is closed and small, a dispatch table or a plain conditional is simpler, faster, and checkable at build time."

  • A team replaced a 12-branch conditional with a 12-handler chain and now reports bugs are harder to find. What do you look at first?
    Whether the guards are mutually exclusive on an exact discriminator. If they are, the chain added a linear scan, an ordering dependency, and eleven files for nothing — a keyed map with an exhaustiveness test restores single-place readability. If guards genuinely overlap, keep the chain but add the missing scaffolding: order owned in one builder, a golden-order test, per-handler match counters, and an unhandled counter, which is usually what makes debugging painful rather than the pattern itself.
  • How do you decide between a chain of handler classes and a data-driven decision table?
    By rule volume, change rate, and authorship. A handful of stable rules that need real code (I/O, complex computation) belong in handler classes. Dozens to hundreds of rules that change on a business cadence, are combinations of simple conditions, and ideally are authored or reviewed by domain experts belong in a table or rules engine, where you can mechanically check for overlapping and missing rows and deploy changes as data rather than code.

Using a chain for an exact-key dispatch is like handing a parcel down a row of clerks asking each "is this yours?" when the parcel already has the room number printed on it — the address book was right there.

saying these in an interview costs you the question

  • Treating 'replace conditional with polymorphism' as unconditionally good, regardless of set size or volatility.
  • Building a chain over an exact, mutually exclusive discriminator where a map is faster and safer.
  • Calling an always-every-stage transformation pipeline a Chain of Responsibility, implying a short-circuit nobody uses.
  • Using a chain for broadcast semantics where Observer/pub-sub is meant, coupling independent subscribers into one sequence.
  • Adopting the pattern without funding the telemetry it needs — startup order log, per-handler counters, unhandled alerts.

context