The Core Idea: A Chain of Handlers
Chain of Responsibility gives a request multiple potential handlers, linked together in a chain, and passes the request along that chain until some handler processes it (or the end of the chain is reached with no handler taking action). Each handler implements a common interface with a method like handle(request) and holds a reference to the next handler in the chain. Inside handle(), a handler first checks whether it can process the request; if so, it does the work and may or may not pass the request further, and if not, it forwards the request unchanged to the next link. This decouples the sender, who just calls handle() on the first handler, from the concrete receiver, since the sender has no idea which handler in the chain will ultimately deal with the request.
Cricket analogy: When a delivery is bowled, it first faces the batter, and if the batter doesn't play a shot it passes to the wicketkeeper, and if that misses too it continues to the boundary — each 'handler' in the ball's path gets a chance to stop it before responsibility passes on.
Implementing the Chain
A typical implementation defines an abstract Handler base class with a setNext(handler) method to build the chain and a handle(request) method containing default forwarding logic (calling next.handle(request) if a next handler exists). Concrete handlers override handle() to add their specific logic, usually calling super.handle(request) or explicitly invoking this.next?.handle(request) when they choose not to fully consume the request. This structure means adding, removing, or reordering handlers only requires changing how the chain is wired together (often in a factory or composition root), not touching the handler classes themselves — a strong application of the Open/Closed Principle.
Cricket analogy: A fielding captain can reorder the slip cordon between overs — moving first slip to gully — without changing how any individual fielder catches the ball, just as reordering a handler chain doesn't require rewriting the handlers themselves.
abstract class Handler {
private next: Handler | null = null;
setNext(handler: Handler): Handler {
this.next = handler;
return handler;
}
handle(request: SupportTicket): string | null {
if (this.next) {
return this.next.handle(request);
}
return null;
}
}
interface SupportTicket {
severity: 'low' | 'medium' | 'critical';
}
class Tier1Handler extends Handler {
handle(request: SupportTicket): string | null {
if (request.severity === 'low') {
return 'Resolved by Tier 1';
}
return super.handle(request);
}
}
class Tier2Handler extends Handler {
handle(request: SupportTicket): string | null {
if (request.severity === 'medium') {
return 'Resolved by Tier 2';
}
return super.handle(request);
}
}
class OnCallEngineerHandler extends Handler {
handle(request: SupportTicket): string | null {
if (request.severity === 'critical') {
return 'Paged the on-call engineer';
}
return super.handle(request);
}
}
const tier1 = new Tier1Handler();
const tier2 = new Tier2Handler();
const onCall = new OnCallEngineerHandler();
tier1.setNext(tier2).setNext(onCall);
console.log(tier1.handle({ severity: 'critical' }));Middleware: A Real-World Application
HTTP middleware pipelines, found in frameworks like Express, Koa, and ASP.NET Core, are a widely used real-world variant of Chain of Responsibility. Each middleware function receives the request, an optional response, and a next() callback; it can inspect or modify the request, short-circuit by sending a response directly (stopping the chain), or call next() to pass control to the following middleware, such as an authentication check followed by a logging step followed by the final route handler. Unlike the classic textbook version where only one handler ultimately 'owns' the request, middleware chains often have every link do some work — logging, compression, authentication — and still call next(), making the chain closer to a pipeline of successive transformations than a search for a single owner.
Cricket analogy: A DRS decision pipeline runs through ball-tracking, then edge detection, then umpire's call thresholds in sequence, with each stage contributing evidence and passing control onward rather than one stage 'owning' the whole decision.
Express.js middleware is Chain of Responsibility in production code: app.use(logger); app.use(authenticate); app.use(routeHandler) builds an explicit chain where each function calls next() to pass control forward.
If no handler in the chain processes a request and there's no explicit fallback/default handler at the end, the request can silently fall through unhandled — always terminate the chain with a default handler or an explicit error response for requests nothing else matched.
- Chain of Responsibility passes a request along a chain of handlers until one processes it.
- Each handler implements a common interface and holds a reference to the next handler.
- The sender is decoupled from which concrete handler ultimately processes the request.
- Chains can be reconfigured (reordered, extended) without modifying the handler classes themselves.
- HTTP middleware pipelines (Express, Koa, ASP.NET Core) are a widely used real-world variant.
- Middleware often differs from the classic pattern in that many/all handlers act and call next(), rather than one handler exclusively owning the request.
- Always terminate a chain with a default/fallback handler to avoid silently unhandled requests.
Practice what you learned
1. What is the defining behavior of the Chain of Responsibility pattern?
2. What does decoupling the sender from the receiver mean in this pattern?
3. Which real-world software mechanism is a widely used variant of Chain of Responsibility?
4. How does the classic Chain of Responsibility differ from a typical middleware pipeline?
5. What is a key risk if a chain has no default/fallback handler at the end?
Was this page helpful?
You May Also Like
Mediator Pattern
A behavioral pattern that centralizes complex communications between related objects into a single mediator object, reducing direct coupling.
Visitor Pattern
A behavioral pattern that lets you add new operations to a family of classes without modifying those classes, by moving the operation into a separate visitor object.
Iterator Pattern
A behavioral pattern that provides a way to access elements of an aggregate object sequentially without exposing its underlying representation.
Related Reading
Related Study Notes in Software Engineering
Browse all study notesMicroservices Study Notes
Software Architecture · 30 topics
Software EngineeringTesting & TDD Study Notes
Software Testing · 30 topics
Software EngineeringSoftware Engineering Study Notes
Python · 40 topics
Software EngineeringGit & Version Control Study Notes
Bash · 40 topics
Software EngineeringSystem Design Study Notes
Architecture · 40 topics