Somewhere, one function knows every approval limit in the company, and every reorganization, threshold change, and new team lands as an edit to it. Chain of Responsibility is the Gang of Four behavioral pattern that links handlers into a chain and passes each request along it until some handler takes responsibility, which decouples the sender from the receivers so thoroughly that the catalog's framing promises the sender no particular receiver at all.
A chain of responsibility is really just a linked list of handlers where each node makes one local decision: deal with the request, or hand it to the next node. The sender talks to the head and walks away. Who actually answered, how many declined first, and whether anyone answered at all are runtime facts the sender never learns.
Here's the centralized version, an expense router with three approval tiers. Read it and notice it's genuinely fine code, which is exactly what makes this pattern easy to overapply. The ladder is short, the rules are static, and the whole policy fits on one screen:
The trouble starts when the rules stop being static. The tiers belong to different teams who each want to own their own limit, departments wire different escalation paths, and a new compliance tier needs inserting for one region only. Every one of those becomes an edit to this single function, by someone who must understand all of it, and the function becomes the org chart's most contested file. The chain version exists for exactly that moment, and not before.
The chain version gives each tier its own class with one local rule, and links them at assembly time. Approver holds the successor pointer and the traversal, ConcreteHandlers supply can_approve() and a title, and the example wires lead to manager to director. Before reading past the code, trace two requests by hand: approve(450) and approve(50000):
The 450 request visits the lead, who declines at a 100 limit, then the manager, whose 1000 limit covers it: manager approved, and the director never hears about it. The 50000 request gets declined by all three, reaches a None successor, and lands in the explicit end-of-chain answer. Now insert a VP tier between manager and director: one new class, one changed wiring line, and zero edits to anything that existed before.
Three roles. The Handler (Approver) declares the handling interface and carries the successor reference plus the forward-by-default traversal. ConcreteHandlers (TeamLead, Manager, Director) supply the local decision. The Client wires the chain and sends requests to the head, knowing nothing past it. One property deserves bold print: nothing guarantees a request gets handled, and the end of the chain is a case you must design, not an edge you can ignore.
The pattern works like phone support tiers: your issue climbs from the first agent to specialists until someone can actually fix it, and each tier only knows who they escalate to. The analogy flatters the pattern in one way worth catching: a call center's final tier is a catch-all that must take whatever arrives, while a raw chain has no such floor. The 50000 expense fell off the end because the code chose a fallback answer, and a chain without that choice just drops the request.
Installing the pattern is four moves:
Approver answers one question: can I handle this? Yes means handle it and stop, no means forward to the successor untouched. Two outcomes, decided locally, with no view of the rest of the lineup._next. The team lead doesn't know a director exists, and that ignorance is the decoupling, since inserting a new level touches two links and zero handler bodies.lead.set_next(Manager()).set_next(Director()) builds the lineup in one visible place, at runtime. Different departments can wire different chains from the same handler classes, which is the flexibility a hardcoded ladder can't offer.None successor, and what happens then is yours to define: a default, an error, an escalation. The pattern guarantees delivery attempts, not delivery, and forgetting that is the classic bug.You might think Express middleware is this pattern verbatim, and the resemblance is real: a lineup of handlers, each holding a reference to the next. Express's own guide describes an app as essentially a series of middleware calls, where each function must call next() or the request is left hanging. The stopping rule is what flips. Textbook chain of responsibility stops at the first handler that takes the request, and forwarding is the default. Middleware runs every handler, and continuing is the default, with ending the cycle as the opt-out. Same skeleton, opposite resting state, and a forgotten next() in middleware is the mirror image of a chain that drops requests off its end.
The browser runs the everyone-runs variant on every click you've ever handled. DOM events bubble from the clicked element up through its ancestors, each level's listeners firing in turn, with stopPropagation() as the explicit kill switch that turns it back into first-handler-wins. Knowing which default you're standing on is most of what these chains ask of you.
Go's standard library ships this pattern as HTTP middleware. The net/http package wraps handlers in handlers: TimeoutHandler, StripPrefix, and MaxBytesHandler each take an inner http.Handler and return a new one, and the community's middleware signature func(http.Handler) http.Handler composes those wrappers into chains. The code tab's expense version uses function fields for the predicate, which is the same idea with the chain made explicit.
Rust has no standard chain type, and the idiomatic shape depends on what's known at compile time. The code tab threads an Option<Box<dyn Approver>> successor through trait objects, ownership flowing down the chain. When the handler set is fixed, many Rust codebases skip the trait machinery for a plain Vec<Box<dyn Handler>> looped until one returns Some, which delivers first-handler-wins with less ceremony and an order you can read top to bottom in one place.
Chains hide their order, and order is frequently behavior. An authentication handler before a logging handler logs only authenticated requests, the reverse order logs everything, and nothing in either handler's code hints that the difference exists. Reorderings that look like harmless refactors ship behavior changes, which is why the wiring deserves one visible home and review attention all out of proportion to its line count.
The other failure is using a chain where nothing varies. Three static if branches with exact rules, owned by one team, changed once a year, want to stay an if ladder, and the dispatcher code at the top of this page was fine until the requirements around it changed. The chain buys runtime assembly, independent ownership, and insertable links, and it charges traversal, indirection, and order-sensitivity. Pay only when you're using what it sells.
The structure is a linked list, and the judgment lives in these five:
Two questions before you go. First: approve(450) returned manager approved. List every method call that happened, in order, including the ones that declined. Second: middleware pipelines and this page's chain are both linked handler lineups, so what's the opposite default between them, and what mechanism flips each one? Both answers are on this page.
Then go spot it in the wild. Every click handler you've written sat on a chain, since DOM events climb the ancestor tree level by level until something stops them. Server middleware stacks, logging frameworks routing records through appender chains, and exception handling itself, where a throw climbs the call stack until a catch takes responsibility, all run the same shape. Last, find the longest if/elif ladder in your codebase and ask two questions of it: do different people own different branches, and does the lineup ever change at runtime? Two yeses and it wants to be a chain. Two noes and it's already correct.
def approve(amount: int) -> str:
# One function knows every approval limit in the company,
# and every team's limit change is an edit right here
if amount <= 100:
return "team lead approved"
elif amount <= 1000:
return "manager approved"
elif amount <= 10000:
return "director approved"
return "needs a board vote"
# Example usage
print(approve(450)) # manager approved
print(approve(50000)) # needs a board votefrom abc import ABC, abstractmethod
class Approver(ABC): # Handler: handle the request, or pass it along
def __init__(self):
self._next: Approver | None = None
def set_next(self, approver: "Approver") -> "Approver":
self._next = approver
return approver # returning it lets chains read left to right
def approve(self, amount: int) -> str:
if self.can_approve(amount):
return self.title() + " approved"
if self._next is None:
# The end of the chain is a design decision, not an accident
return "fell off the chain: needs a board vote"
return self._next.approve(amount)
@abstractmethod
def can_approve(self, amount: int) -> bool: ...
@abstractmethod
def title(self) -> str: ...
class TeamLead(Approver): # ConcreteHandler
def can_approve(self, amount: int) -> bool:
return amount <= 100
def title(self) -> str:
return "team lead"
class Manager(Approver): # ConcreteHandler
def can_approve(self, amount: int) -> bool:
return amount <= 1000
def title(self) -> str:
return "manager"
class Director(Approver): # ConcreteHandler
def can_approve(self, amount: int) -> bool:
return amount <= 10000
def title(self) -> str:
return "director"
# Example usage: wire the chain once, send everything to the head
lead = TeamLead()
lead.set_next(Manager()).set_next(Director())
print(lead.approve(450)) # manager approved
print(lead.approve(50000)) # fell off the chain: needs a board vote