Chain of Responsibility
Think of an expense approval process at a company. A $50 request only needs a manager's sign-off. A $5,000 request needs a director. A $50,000 request needs a VP. The person submitting the request doesn't route it themselves — they just hand it to their manager, and it climbs the chain until someone with enough authority handles it. The Chain of Responsibility pattern is that exact idea in code: a request travels along a chain of handler objects until one of them decides it can handle it, and the sender never needs to know which one that will be.
⚠️ The Problem
Imagine building that expense approval system directly: a single function with a wall of
if/else checks — if (amount <= 1000) { manager.approve(); } else if
(amount <= 10000) { director.approve(); } else { vp.approve(); }. Every time the
company adds a new approval tier, or changes a threshold, or inserts a new role between two
existing ones, you have to go back and edit this one function. The sender of the request is
also tightly coupled to every possible approver, even though it only ever needs one of them to
actually act.
✅ The Solution
Give each handler (Manager, Director, VP) a reference to the "next" handler in the chain,
and a consistent method like handle(request). Each handler checks whether it can
approve the request; if it can, it does and stops the chain; if it can't, it forwards the
request to the next handler by calling next.handle(request). The submitter just
calls chain.handle(request) on the first handler — it never decides who
ultimately approves it, and adding a new tier means creating one more handler and linking it
in, with zero changes to the handlers that already exist.
Example 1 — an expense approval chain
class Approver {
constructor(name, limit) {
this.name = name;
this.limit = limit;
this.next = null;
}
setNext(handler) {
this.next = handler;
return handler;
}
handle(request) {
if (request.amount <= this.limit) {
console.log(`${this.name} approved $${request.amount} for "${request.reason}"`);
return;
}
if (this.next) {
this.next.handle(request);
} else {
console.log(`No one could approve $${request.amount} — request rejected.`);
}
}
}
const manager = new Approver("Manager", 1000);
const director = new Approver("Director", 10000);
const vp = new Approver("VP", 100000);
manager.setNext(director).setNext(vp);
manager.handle({ amount: 500, reason: "Team lunch" }); // Manager approves
manager.handle({ amount: 7500, reason: "Conference travel" }); // Director approves
manager.handle({ amount: 45000, reason: "New server rack" }); // VP approves
The code that submits a request only ever talks to manager, the first link in
the chain. It has no idea whether a Director or VP even exists, and if the company later inserts
a "Senior Director" tier between Director and VP, only one line — the wiring — needs to
change.
Example 2 — a secondary context: HTTP middleware
public abstract class Middleware
{
protected Middleware? Next;
public Middleware SetNext(Middleware next)
{
Next = next;
return next;
}
public abstract void Handle(HttpRequest request);
}
public class AuthMiddleware : Middleware
{
public override void Handle(HttpRequest request)
{
if (!request.HasValidToken)
{
Console.WriteLine("401 Unauthorized — request stopped.");
return;
}
Console.WriteLine("Auth passed.");
Next?.Handle(request);
}
}
public class LoggingMiddleware : Middleware
{
public override void Handle(HttpRequest request)
{
Console.WriteLine($"Logging request to {request.Path}");
Next?.Handle(request);
}
}
public class RateLimitMiddleware : Middleware
{
public override void Handle(HttpRequest request)
{
if (request.RequestsThisMinute > 100)
{
Console.WriteLine("429 Too Many Requests — request stopped.");
return;
}
Console.WriteLine("Rate limit OK — handing off to route handler.");
Next?.Handle(request);
}
}
var auth = new AuthMiddleware();
var logging = new LoggingMiddleware();
var limiter = new RateLimitMiddleware();
auth.SetNext(logging).SetNext(limiter);
auth.Handle(new HttpRequest("/api/orders", hasValidToken: true, requestsThisMinute: 12));
This is exactly how real web frameworks structure middleware pipelines — each middleware decides whether to short-circuit the chain (stop it, like a failed auth check) or pass control to the next link. The route handler at the very end never has to re-check auth, logging, or rate limits itself.
👍 When To Use It
- More than one object might handle a request, and which one handles it should be decided at runtime rather than hardcoded by the sender.
- You want to issue a request without specifying the receiver explicitly — the sender only needs to know about the first handler in the chain.
- The set of handlers, or their order, needs to change independently of the code that issues requests — new handlers can be inserted or removed without touching senders.
👎 When NOT To Use It
- When exactly one object should always handle every request — a direct call is simpler and doesn't need a chain to express that.
- When it's important to guarantee a request is actually handled — a naive chain can let a request silently fall off the end if no handler claims it, which is easy to overlook.
- When the chain gets long enough that debugging "which handler actually processed this" becomes hard — long chains trade a single if/else block for a different kind of hidden control flow, and that tradeoff isn't always a win.
❓ FAQ
How is this different from Decorator?
Structurally, both wrap objects in a linked sequence. But Decorator's whole point is that every layer in the chain runs and adds behavior — the request/response always passes through all of them. Chain of Responsibility is about choosing which single handler (or subset) actually processes the request, and any handler can stop the chain early once it's satisfied.
Can more than one handler process the same request?
Yes — nothing forces a handler to stop the chain after acting. Middleware pipelines are
a good example: logging middleware can act (log the request) and still call
next() to let the rest of the chain run too. Whether the chain stops after
the first match or keeps going is a design choice you make per chain.
What happens if no handler in the chain can handle the request?
By default, nothing — the request falls off the end silently, which is a common bug source. Most real implementations add an explicit fallback: either the last handler in the chain is a catch-all that always handles the request, or the chain itself throws/logs an error when it reaches the end with nothing having claimed the request.
🙋 There Are No Dumb Questions
Q: Isn't this just a linked list of if-statements?
A: Structurally, yes — that's a fair way to think about it. The value isn't the linked-list
mechanics, it's that each handler is a separate, independently testable object that only knows
about "can I handle this, and who's next," instead of one giant function that has to know
about every case at once.
✅ Quick Check
In Chain of Responsibility, what does the object that issues a request need to know?
Next up: what if you need to treat an action itself as an object — something you can store, queue, log, or undo, instead of just calling a function and having it happen immediately? That's the Command pattern.