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

Expense approvals travelling up a chainTry it yourself
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

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.