Chain of Responsibility passes a request along a series of handlers until one of them deals with it. The sender does not know which handler will respond, and handlers do not know about each other.
You meet it constantly as middleware: every web framework's request pipeline, every logging framework's appender chain, every event system's bubbling. The pattern is more familiar than its name.
The problem
function handleRequest(req) {
if (isBlockedIp(req)) return reject('blocked');
if (!hasValidToken(req)) return reject('unauthenticated');
if (!isAuthorised(req)) return reject('forbidden');
if (exceedsRateLimit(req)) return reject('rate limited');
if (!isValidPayload(req)) return reject('bad request');
return route(req);
}This works, and it fails in familiar ways: the list grows, adding a check edits a function that already works, no check can be reused or tested alone, and the set cannot be reordered or configured per route.
The pattern
Each check becomes a handler that either deals with the request or passes it on.
const pipeline = [blockList, authenticate, authorise, rateLimit, validate];
function handle(req) {
for (const handler of pipeline) {
const result = handler(req);
if (result) return result; // handled — stop
}
return route(req); // nobody objected
}Diagram, as text
- Request leads to Block list
- Block list leads to Authenticate (passes)
- Authenticate leads to Authorise (passes)
- Authorise leads to Rate limit (passes)
- Rate limit leads to Validate (passes)
- Validate leads to Route to handler (passes)
- Block list leads to Reject (blocked)
- Authenticate leads to Reject (no token)
- Authorise leads to Reject (forbidden)
- Rate limit leads to Reject (too many)
The classic formulation has each handler hold a reference to the next. A list with a loop, as above, is the same pattern with the linking extracted — easier to reorder, easier to inspect, and it does not bury the chain's shape inside the handlers.
Two shapes with different semantics
First-match wins. One handler deals with the request and the chain stops. Support-ticket escalation, exception handlers, routing.
Everyone runs, any may stop. Each handler processes and passes along, and any may terminate. This is middleware, and it differs in an important way: handlers can act on the way out as well as on the way in.
const withTiming = (next) => (req) => {
const start = performance.now();
const res = next(req); // downstream runs here
record(performance.now() - start); // on the way back
return res;
};That two-phase shape is Decorator, and the resemblance is not coincidental — middleware is Chain of Responsibility and Decorator meeting in the middle. The distinction that survives: Chain of Responsibility contemplates a handler declining, Decorator does not.
The unhandled request
The pattern's defining hazard: what happens when nobody handles it?
The catalogue leaves this open, and implementations that leave it open produce the worst kind of bug — a request that silently does nothing, returns undefined, or falls through to a default that was never meant to be a catch-all. There is no error, no log line, and the symptom appears far away.
Three defensible policies, and you must choose one:
- A terminal handler that always handles, such as the router above. The chain cannot fall through because the last element is total.
- Explicit failure. Falling off the end throws, so an unhandled request is loud.
- A documented default, logged and counted, so "nothing handled this" is visible in metrics.
Silence is the only indefensible option.
Where it fits and where it does not
It fits when handlers are genuinely independent, when the set changes, when different contexts need different subsets — public routes skipping authentication, internal routes skipping rate limits — and when handlers should be individually testable.
It does not fit when the "handlers" are steps that all must run in a fixed order with no possibility of declining. That is a pipeline or a Template Method, and calling it a chain suggests an optionality that does not exist.
It also does not fit when handlers need each other's results. A handler that must know what an earlier one decided is not independent, and threading a shared mutable context through the chain to carry that information is how these designs become unreadable.
Check your understanding
Common mistakes
Undefined fall-through. A request nobody handles must fail loudly or hit a total terminal handler.
Treating order as arbitrary. Every adjacency encodes a decision, most visibly in security.
Handlers that depend on earlier handlers' annotations. Undeclared coupling that breaks silently on reorder or reuse.
A shared mutable context that grows. Once a dozen handlers annotate it, nobody can say what is present when.
Using it for steps that all must run. That is a pipeline, and calling it a chain implies an optionality that does not exist.
Handlers with side effects that fire before a later handler rejects. Logging a request as accepted before authorisation rejects it produces misleading data.
No way to see the chain. If the composition is assembled across several files, the effective order is unknowable.
Practice
Design the middleware chain for an API gateway serving public, authenticated and internal routes. Give the order, justify the security-relevant adjacencies, and explain how different routes get different chains.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
Work out which handlers must run for every request regardless of route, and which are route-dependent.
Show a strong answer
Order for an authenticated route, outermost first: request id → structured logging → panic recovery → coarse IP rate limit → CORS/preflight → body size limit → authenticate → per-user rate limit → authorise → schema validation → route handler.
The security-relevant adjacencies, which are the substance of the question:
- Coarse IP limit before authentication. Authentication costs a token verification or a database lookup, and an unauthenticated flood must be shed before that cost is paid. This limit is keyed to the IP because that is the only thing known to be true at this point.
- Per-user limit after authentication, not before. A limit keyed to a claimed identity before verification lets anyone deny service to a named user. Two limits at two points is the correct design and it is frequently implemented as one in the wrong place.
- Body size limit before anything that reads the body. Otherwise a large payload is buffered before being rejected, which is the denial-of-service the limit exists to prevent.
- Authenticate before authorise. Trivially, authorisation needs an identity.
- Validation after authorisation. Detailed validation errors describe your payload structure; returning them to callers who are not authorised for the endpoint leaks the API's shape to people who should not see it. This one is routinely got backwards because validation feels cheap.
- Panic recovery outside almost everything so a failure anywhere below becomes a 500 rather than a dropped connection — but inside logging and request id, so the failure is logged with its correlation id.
- Logging outermost. It must record requests that are rejected by every subsequent handler, including the rate limiter. Placed lower, exactly the traffic you most want to see becomes invisible.
How routes get different chains:
- Compose named stages rather than editing one list:
PUBLIC = [...base],AUTHENTICATED = [...base, authenticate, userRateLimit, authorise],INTERNAL = [...base, mutualTls, internalAuthorise]. The base is shared and cannot be forgotten. - Make the base non-optional in the type or the builder, so a new route cannot be registered without it. The failure mode to design against is a route added in a hurry that skips logging and rate limiting — and that happens by omission, not by intent.
- Expose the effective chain per route as a debug endpoint or a start-up log. If nobody can see the order, nobody can review it, and every adjacency above is unverifiable.
One thing I would test explicitly: that an unauthenticated request to an authenticated route produces a 401 without the validation error body. That single test pins the ordering decision most likely to be silently reversed later.
A team uses a chain for document approval: each approver in sequence either approves, rejects, or passes to the next based on the amount. Approvals are being lost — some documents reach the end with no decision. Diagnose and redesign.
Write your own answer first, on paper or in a note. Then compare.
Show a hint
This is the fall-through problem, but ask also whether a chain is the right model for approvals at all.
Show a strong answer
The immediate diagnosis is undefined fall-through. A document whose amount exceeds every approver's limit — or matches none of the conditions, perhaps because of a gap between bands — reaches the end and nothing happens. There is no error because the loop simply finishes. The document sits in a pending state indefinitely, and nobody notices until someone asks about it, which is why these are found weeks later.
The immediate fixes:
- A terminal handler that always handles. The last element escalates to a named fallback approver or a queue a human monitors. The chain then cannot fall through by construction.
- Alert on documents pending beyond a threshold, which catches this and every future variant of "stuck in the workflow". This is the control that should exist regardless of the design.
- Check the bands for gaps and overlaps with a test that asserts every amount from zero to the maximum matches exactly one approver. Off-by-one gaps between bands are the classic source.
But I would question the model, because the symptom suggests a deeper mismatch. Chain of Responsibility fits when the sender does not care who handles the request and handlers are interchangeable in kind. Approval is not like that:
- Who approved matters and must be recorded. That is not a property the pattern is concerned with.
- The chain is often not "first match wins" in reality. Above a threshold, most organisations want several approvals, not the first one that qualifies. A chain expresses "someone dealt with it", which is the wrong semantic.
- Approvals are asynchronous and long-lived. A person takes two days. A synchronous chain models an in-process decision, and stretching it across days means the chain is really a persisted workflow with the chain shape imposed on it.
- Rejections and recalls need to go somewhere. A chain has no vocabulary for going back.
What I would build instead: an explicit approval policy that, given a document, returns the set of approvals required — which may be one approver or several, and may depend on amount, department and document type. Then a workflow tracks which of those have been obtained, with a state machine per document. This makes the required set inspectable before anything is sent, which is the property that prevents the original bug entirely: you can tell at submission time whether anyone can approve this document, rather than discovering at the end that nobody did.
What I would keep from the chain: nothing structural, but the routing rules themselves are sound and become the policy's implementation. The refactor is about changing what the structure means, not rewriting the business rules.
Key takeaways
- Chain of Responsibility passes a request along handlers until one takes it — middleware is its most common form.
- Decide what happens when nobody handles it. A total terminal handler, an explicit throw, or a logged default; silence is indefensible.
- Two shapes: first-match wins, and everyone runs with any able to stop — the second allows work on the way out, which is where it meets Decorator.
- Order is part of the contract, especially for security: coarse limits before authentication, per-user limits after, validation after authorisation.
- Annotating a shared context creates undeclared dependencies. Keep annotations few, named, and traceable to their producer.
- Compose named stages so route-specific chains cannot omit the mandatory base, and make the effective order visible.
- If handlers all must run or need each other's results, it is a pipeline, not a chain.
Further reading
- Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (Addison-Wesley, 1994).
- Gregor Hohpe and Bobby Woolf, Enterprise Integration Patterns (Addison-Wesley, 2003) — pipes and filters, and message routers.
- Frank Buschmann and others, Pattern-Oriented Software Architecture, Volume 1 (Wiley, 1996).
- Michael Nygard, Release It!, second edition (Pragmatic Bookshelf, 2018) — shedding load at the right layer.
- Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley, 2002) — intercepting filters.