Chain of Responsibility Pattern
Concept
Chain of Responsibility decouples a request's sender from the code that eventually handles it by threading the request through a linked sequence of handler objects. Each handler gets the same chance: inspect the request, decide whether it's the right one to act, and either process it (optionally stopping the chain) or pass it along to the next handler. The sender only ever knows about the head of the chain — it has no idea which handler, if any, will end up doing the work, and the chain's membership and order can change independently of the sender's code.
This is one of the more commonly-used GoF patterns today precisely because most engineers already build it without naming it: an HTTP middleware stack (auth → CORS → rate limiting → logging → route handler) is a Chain of Responsibility where each middleware either short-circuits the request (auth failure → 401, never reaching the route handler) or calls next() to hand off. Express, Django, ASP.NET Core, and most API gateways structure request processing this way natively. Support-ticket escalation (L1 → L2 → L3 → engineering) and event-bubbling in UI frameworks (a click event rising through parent DOM nodes until something calls stopPropagation()) are the same shape outside of pure request/response handling.
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
| Chain of Responsibility | Sender is fully decoupled from handlers; chain composition/order changes without touching the sender or other handlers; each handler is small and independently testable | Debugging is harder — following a request through N handler objects means stepping through indirection instead of reading one function; nothing guarantees a request gets handled at all (a chain with no matching handler silently does nothing unless you add an explicit fallback) |
One large conditional (if/elif cascade in the sender) |
Single place to read the entire decision logic; easy to trace in a debugger | Sender is coupled to every case; adding a new case means editing code that already works, risking regressions in unrelated branches; violates open/closed as the case list grows |
| Observer (broadcast to all listeners) | Every interested party sees the event, no single point of failure for "did anyone see this" | Wrong model when exactly one handler should act and stop the flow — Observer has no notion of "handled, stop propagating," so you'd need extra coordination to prevent double-handling |
The natural comparison is against Decorator, which shares almost the same structural shape (a chain of wrapper objects). The difference is intent: Decorator's whole point is that every layer in the chain runs and adds behavior (logging wraps caching wraps the raw call — all three always execute). Chain of Responsibility's point is the opposite — each link is a candidate that may or may not act, and a link that handles the request can stop the chain, meaning downstream handlers never run at all.
When to use / when not to
- Use when a request should be handled by exactly one of several possible handlers, and which one depends on runtime conditions the sender shouldn't need to know about — middleware pipelines, validation pipelines, event-bubbling UI systems, support/approval escalation chains.
- Especially valuable when the set of handlers changes over time or per-deployment (feature flags adding/removing a validation step, a plugin system registering new middleware) — the chain can be reconfigured without touching the sender.
- Don't reach for it when there's a small, fixed, unlikely-to-grow set of cases and a plain conditional communicates the logic more directly — the pattern's indirection isn't free, and a three-branch
ifdoesn't need to become three classes. - Don't use it when every handler needs to run unconditionally (that's Decorator, not Chain of Responsibility) or when multiple handlers legitimately need to react to the same request simultaneously (that's closer to Observer/pub-sub).
Common pitfall
Building a chain with no explicit terminal case, so a request that no handler claims just silently falls off the end of the chain and vanishes — no error, no log, no result. This is especially dangerous in middleware stacks: a misconfigured or reordered chain can produce requests that are quietly dropped rather than failing loudly. The fix is a mandatory final handler (a catch-all that logs or raises on anything unhandled) rather than trusting that "some handler will always match."
Engineering Lens
The pattern's real value shows up in code review as a specific question: "if I add a new case, do I touch existing code, or do I add a new file?" A well-built chain answers "add a new file" — a new handler that slots in without anyone needing to re-read or re-test the handlers around it. That's the open/closed principle made concrete, and it's the same reasoning that makes middleware architectures (auth, rate limiting, tracing) the default shape for HTTP services rather than one large request-dispatch function: each concern is isolated, independently ordered, and independently testable, at the cost of needing to actually trace a request through several files to understand end-to-end behavior — a cost worth paying once the number of cases outgrows what a single function can hold clearly.
Related
- Circuit Breaker Pattern — another behavioral wrapper around a call, but with different intent (fail fast on a known-bad dependency vs. route to the right handler)
- REST — HTTP middleware stacks are Chain of Responsibility in practice