Design Patterns, Explained Simply · Behavioral Patterns: How Objects Share Work
Chain of Responsibility
Pass a request along a chain of handlers until one of them deals with it, so the sender doesn't need to know who will.
The problem
Expense claims need approval. A team lead can approve up to 1,000, a director up to 5,000, the CFO up to 20,000, and anything larger goes to the board. The first version is one function with a growing if amount <= 1000 ... elif ... elif ... chain that also knows every approver's name and rules.
When the company adds a "finance check" for claims above 3,000, or a team lead goes on holiday and their approvals should go to a deputy, someone has to carefully edit that function again.
Real-world analogy
Calling customer support. The first-line agent handles simple questions. If they can't, they pass you to a specialist, who might pass you to an engineer. You make one call; the request travels along the chain until someone can deal with it.
Without the pattern
def approve(amount):
if amount <= 1000:
return "team lead"
elif amount <= 5000:
return "director"
elif amount <= 20000:
return "CFO"
return "board" # every rule change edits this one function
The pattern
- Handler interface:
handle(request)plus a reference to the next handler. - Concrete handlers: each either handles the request or passes it to the next one. A handler can also do some work and then pass it on.
- Client: builds the chain once and sends requests to its first handler.
The chain can be rearranged, extended or shortened at runtime without changing the handlers or the client.
The code
class Approver:
def __init__(self, title, limit):
self.title, self.limit = title, limit
self.next = None
def then(self, nxt): # link handlers; returns the next one for chaining
self.next = nxt
return nxt
def handle(self, amount):
if amount <= self.limit:
return f"{amount} approved by {self.title}"
if self.next is None:
return f"{amount} needs a board vote"
return self.next.handle(amount) # pass it along
lead = Approver("team lead", 1000)
lead.then(Approver("director", 5000)).then(Approver("CFO", 20000))
for amount in [300, 4200, 18000, 75000]:
print(lead.handle(amount))
Output
300 approved by team lead 4200 approved by director 18000 approved by CFO 75000 needs a board vote
class Approver {
private next: Approver | null = null;
constructor(private title: string, private limit: number) {}
then(next: Approver): Approver { // link handlers; returns the next one for chaining
this.next = next;
return next;
}
handle(amount: number): string {
if (amount <= this.limit) return `${amount} approved by ${this.title}`;
if (this.next === null) return `${amount} needs a board vote`;
return this.next.handle(amount); // pass it along
}
}
const lead = new Approver("team lead", 1000);
lead.then(new Approver("director", 5000)).then(new Approver("CFO", 20000));
for (const amount of [300, 4200, 18000, 75000]) console.log(lead.handle(amount));
Output
300 approved by team lead 4200 approved by director 18000 approved by CFO 75000 needs a board vote
public class Main {
public static void main(String[] args) {
Approver lead = new Approver("team lead", 1000);
lead.then(new Approver("director", 5000)).then(new Approver("CFO", 20000));
for (int amount : new int[]{300, 4200, 18000, 75000}) System.out.println(lead.handle(amount));
}
}
class Approver {
private final String title;
private final int limit;
private Approver next;
Approver(String title, int limit) { this.title = title; this.limit = limit; }
Approver then(Approver next) { // link handlers; returns the next one for chaining
this.next = next;
return next;
}
String handle(int amount) {
if (amount <= limit) return amount + " approved by " + title;
if (next == null) return amount + " needs a board vote";
return next.handle(amount); // pass it along
}
}
Output
300 approved by team lead 4200 approved by director 18000 approved by CFO 75000 needs a board vote
Run this on your computer — see “Get Started” for setup.
<?php
class Approver {
private ?Approver $next = null;
public function __construct(private string $title, private int $limit) {}
public function then(Approver $next): Approver { // link handlers; returns the next one for chaining
$this->next = $next;
return $next;
}
public function handle(int $amount): string {
if ($amount <= $this->limit) return "$amount approved by {$this->title}";
if ($this->next === null) return "$amount needs a board vote";
return $this->next->handle($amount); // pass it along
}
}
$lead = new Approver("team lead", 1000);
$lead->then(new Approver("director", 5000))->then(new Approver("CFO", 20000));
foreach ([300, 4200, 18000, 75000] as $amount) echo $lead->handle($amount), "\n";
Output
300 approved by team lead 4200 approved by director 18000 approved by CFO 75000 needs a board vote
To add the finance check, insert a new handler into the chain. None of the existing approvers change.
When to use it
- Several objects might handle a request, and which one should is decided at runtime.
- You want to process a request through ordered steps that can each stop it: validation, authentication, rate limiting, spam filtering.
- The set or order of handlers should be configurable without touching the client.
When not to use it
- There is always exactly one handler. Call it directly.
- A request might fall off the end of the chain unhandled without anyone noticing. Always define the end-of-chain behaviour (a default handler or an error).
- Long chains are harder to debug: "who handled this?" is not obvious from the code. Logging each step helps.
You've already used it
- HTTP middleware: Express, Laravel, Django and ASP.NET pipelines are chains where each middleware can respond or call
next(). - DOM event bubbling: a click travels from the target element up through its parents until a handler stops it.
- Java servlet filters and Spring Security filter chains.
- Logging: Python's logger hierarchy passes records up to parent loggers.
Key points
- Chain of Responsibility passes a request along a chain until a handler deals with it.
- The sender doesn't know which handler will respond; handlers don't know the whole chain.
- Handlers can be added, removed or reordered without changing the others.
- Always decide what happens when nobody handles the request.
- Further reading: Refactoring Guru: Chain of Responsibility.
Practice questions
What problem does Chain of Responsibility solve?
Answer
It decouples the sender of a request from its receiver. The request travels along a chain of handlers; each one either handles it or passes it on. Handlers can be added, removed or reordered without changing the sender or the other handlers.
Give two examples of Chain of Responsibility in web development.
Answer
HTTP middleware pipelines (Express, Laravel, Django, ASP.NET), where each middleware can respond or call the next one, and DOM event bubbling, where an event travels up the element tree until a handler stops it.
What is a common pitfall of Chain of Responsibility?
Answer
A request can reach the end of the chain without being handled, and nothing reports it. Always define end-of-chain behaviour, such as a default handler or an explicit error.
Open in the app to mark lessons complete, take notes and track your progress.