Chain of Responsibility
Pass a request along a line of handlers until one deals with it — the sender never needs to know which one will.
Big idea
Chain of Responsibility lines up handlers that each get a chance at a request. Each one either handles it or passes it to the next. The sender talks only to the first link, so you can add, remove, or reorder handlers without the sender knowing.
The problem it solves
Expense claims need approval: a team lead can approve up to 500, a director up to 5,000, the CFO up to 50,000, and anything bigger goes to the board. Written as one function, that's an if amount <= 500 … elif … ladder that also has to know who's on holiday, which departments skip the director, and so on. Every policy change edits the same function; testing one rule means setting up all of them.
Think of it like
A customer support line. Your call reaches the first-line agent; if they can't fix it, they escalate to a specialist; if the specialist can't, it goes to engineering. You dialled one number. You don't know — or care — who finally solved it, and the company can add a new tier without changing its phone number.
Structure
In code
class Approver:
def __init__(self, name, limit):
self.name, self.limit, self.next = name, limit, None
def set_next(self, approver):
self.next = approver
return approver # lets you chain the set_next calls
def handle(self, amount, purpose):
if amount <= self.limit:
return f"{amount:>6} for {purpose}: approved by {self.name}"
if self.next is None:
return f"{amount:>6} for {purpose}: rejected, nobody can approve it"
return self.next.handle(amount, purpose)
class OnHoliday(Approver): # a handler that always passes on
def handle(self, amount, purpose):
return self.next.handle(amount, purpose)
lead = Approver("team lead", 500)
lead.set_next(Approver("director", 5_000)).set_next(Approver("CFO", 50_000))
for amount, purpose in [(120, "taxi"), (3_200, "conference"), (40_000, "servers"), (90_000, "office")]:
print(lead.handle(amount, purpose))
# the director is away: splice a pass-through in, the sender doesn't change
lead2 = Approver("team lead", 500)
lead2.set_next(OnHoliday("director", 0)).set_next(Approver("CFO", 50_000))
print(lead2.handle(3_200, "conference"))output
120 for taxi: approved by team lead
3200 for conference: approved by director
40000 for servers: approved by CFO
90000 for office: rejected, nobody can approve it
3200 for conference: approved by CFORun and edit it in the app.
Watch it
Variations
- First handler wins (above) — the request stops once handled.
- Every handler runs — a pipeline where each may act and still pass on; web middleware works this way (auth → rate limit → logging → handler), and any handler may short-circuit with an error.
- Chain from a tree — a UI click bubbles from a button to its panel to the window until something handles it; the "next" link is the parent in a Composite.
What does the sender of a request know in Chain of Responsibility?
How to implement it
- Define a handler interface with
handle(request), and a field for the next handler. - Put "pass it on" logic in a base class so concrete handlers only decide can I handle this?
- Build the chain at the edges — configuration or a factory — not inside handlers.
- Decide what happens at the end of the chain: a default handler, an error, or a rejection.
When to use it — and when not to
Use it when several objects might handle a request and the right one depends on runtime conditions, when the set or order of handlers should be configurable, or for request pipelines (middleware, event bubbling, validation steps). Avoid it when exactly one handler is always responsible — a direct call is clearer — or when you need a guarantee that every request is handled and can't afford an unhandled fall-through.
Pros and cons
| Pros | Cons |
|---|---|
| Sender decoupled from receivers | A request may fall off the end unhandled |
| Handlers added or reordered freely (Open/Closed) | Harder to see which handler acted |
| Each rule is its own small class | Long chains cost time on every request |
How it relates
- Decorator has the same wrapping shape, but decorators always forward; chain links may stop the request.
- Command objects are often what travels along the chain.
- Composite parent links frequently form the chain.
- Mediator and Observer are the other ways to connect senders and receivers: one central hub, and broadcast to all subscribers.
In web middleware, the auth step rejects a request with a 401. What happens to the steps after it?
Key terms
- Chain of Responsibility — Handlers linked in sequence, each handling a request or passing it to the next.
- Handler — An object that can process a request or forward it.
- Short-circuit — A handler ending the chain early, for example by rejecting a request.
- Middleware — A chain of handlers around a web request, each able to act or stop it.