Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle it, passing the request along a chain until someone does.
01
The problem Chain of Responsibility solves
The problem
Expense approvals. A team lead can sign off up to £500. A manager up to £5,000. A director up to £50,000. Above that, it goes to the board.
So the submit button grows a decision: if (amount <= 500) leadApproves(); else if (amount <= 5000) managerApproves(); else if …. Every screen that can submit an expense copies that ladder. Finance changes the manager limit to £7,500 and you have four copies to find.
Then it gets worse, because the real rules aren't only about amount. Anything involving a new vendor needs procurement, regardless of size. Anything over £10,000 in Q4 needs the CFO. Travel above £2,000 needs an extra sign-off. Now the if ladder isn't a ladder, it's a lattice — one method, forty lines, six unrelated reasons to change, and no way to add "procurement checks new vendors" without editing the same method everyone else edits.
02
How the Chain of Responsibility pattern works
Turn each branch of the ladder into its own object, and stand them in a line:
Every handler implements the same interface and gets a reference to the next handler in the chain.
A handler receives the request and asks one question: is this mine? If yes, it handles it and the chain stops. If no, it passes it on, unchanged.
The sender knows only the first handler. It has no idea how long the chain is or who ends up approving.
The chain is assembled in one place, at configuration time — so reordering the rules or inserting procurement between two existing steps is a change to that one place.
Decide what "nobody handled it" means before you write the first handler. This is the pattern's one genuinely dangerous property: a request can reach the end of the chain and simply stop, having done nothing, with no exception and no log line. In an approvals system that's an expense that silently never gets approved. Either terminate the chain with a default handler that always accepts (even if it only escalates to a human), or make falling off the end throw. Never let it return quietly.
Participants. The Handler interface declares the operation and holds the next link. Each Concrete Handler either handles the request or forwards it. The Client sends to the head of the chain only. The green terminal handler is not part of GoF's diagram but belongs in every production chain — it's what turns "nobody handled it" from a silent no-op into a decision.
03
See it: build the chain
Add approvers to the chain and watch it form. The submit button always calls the same thing — the head of the chain — no matter how many links you add or in what order.
▶ Try it — assemble the approval chain
An interactive handler chain: add team lead, manager, procurement, director and a board fallback, and watch the chain form as ExpenseForm → TeamLead → Manager → Director. The form only ever calls the first link, and anything no handler accepts falls off the end.
Two things to try. First, add Procurement after Manager and then think about a £200 order from a brand-new vendor: the team lead takes it at £200 and procurement never sees it. Order is policy, and it's invisible in the type system. Second, build a chain without the board fallback and ask what a £120,000 expense does — the answer is "nothing at all, quietly", which is exactly the failure this pattern is famous for.
04
Chain of Responsibility code examples
Each handler answers one question — is this mine? — and the chain is assembled in one place.
public abstract class Approver {
private Approver next;
public Approver linkTo(Approver next) {
this.next = next;
return next; // returns the LINK, so wiring reads in order
}
public final Decision review(Expense expense) {
if (canHandle(expense)) {
return approve(expense);
}
if (next == null) {
// Never return quietly. An expense that vanishes is worse than one
// that's rejected, because nobody finds out for a month.
throw new NoApproverFound(expense);
}
return next.review(expense);
}
protected abstract boolean canHandle(Expense e);
protected abstract Decision approve(Expense e);
}
public final class ManagerApprover extends Approver {
protected boolean canHandle(Expense e) { return e.amount().lte(Money.gbp(5_000)); }
protected Decision approve(Expense e) { return Decision.approvedBy("manager", e); }
}
// Assembled once, in one place:
Approver head = new TeamLeadApprover();
head.linkTo(new ManagerApprover())
.linkTo(new DirectorApprover())
.linkTo(new BoardApprover()); // always accepts — terminates the chain
head.review(expense);
// Worth knowing: the linked-list version above is GoF's, but most modern Java
// writes `List<Approver>` and a loop — same pattern, no `next` field to wire
// wrong, and the order is visible as a list you can print and test.
from typing import Callable, Iterable
# Python rarely builds the linked list. A list of predicates-and-actions is the
# same pattern with less machinery, and the ORDER is right there in the source.
Approver = tuple[str, Callable[[Expense], bool], Callable[[Expense], Decision]]
CHAIN: list[Approver] = [
("team lead", lambda e: e.amount <= 500, lambda e: Decision("team lead", e)),
("manager", lambda e: e.amount <= 5_000, lambda e: Decision("manager", e)),
("director", lambda e: e.amount <= 50_000, lambda e: Decision("director", e)),
("board", lambda e: True, lambda e: Decision("board", e)),
]
def review(expense: Expense, chain: Iterable[Approver] = CHAIN) -> Decision:
for name, handles, approve in chain:
if handles(expense):
return approve(expense)
raise NoApproverFound(expense) # only reachable if you drop the fallback
# The standard library uses the classic form: a logging.Logger passes a record
# to its own handlers, then to its PARENT logger's handlers, all the way up to
# the root — unless a logger sets `propagate = False`, which cuts the chain.
# Duplicated log lines are almost always this pattern misconfigured.
#include <memory>
#include <optional>
class Approver {
public:
virtual ~Approver() = default;
void setNext(std::shared_ptr<Approver> next) { next_ = std::move(next); }
Decision review(const Expense& e) {
if (canHandle(e)) return approve(e);
if (!next_) throw NoApproverFound{e};
return next_->review(e);
}
protected:
virtual bool canHandle(const Expense&) const = 0;
virtual Decision approve(const Expense&) = 0;
private:
std::shared_ptr<Approver> next_;
};
// A vector of std::function is usually the better C++ too — no virtual
// dispatch, no ownership graph, and the chain is data you can build at runtime:
//
// using Rule = std::function<std::optional<Decision>(const Expense&)>;
// std::vector<Rule> chain = { teamLead, manager, director, board };
//
// for (const auto& rule : chain)
// if (auto d = rule(expense)) return *d;
// throw NoApproverFound{expense};
//
// C++ exception handling is itself a chain of responsibility: a throw walks up
// the stack offering the exception to each enclosing catch until one matches.
// ASP.NET Core's middleware pipeline IS this pattern, and it's the version most
// C# developers meet first. Each component gets the request and decides whether
// to handle it or call next().
public interface IApprover
{
Decision Review(Expense expense);
}
public sealed class ManagerApprover(IApprover next) : IApprover
{
public Decision Review(Expense expense) =>
expense.Amount <= 5_000m
? Decision.ApprovedBy("manager", expense)
: next.Review(expense);
}
public sealed class BoardApprover : IApprover // the terminator
{
public Decision Review(Expense expense) => Decision.ApprovedBy("board", expense);
}
// Built inside-out, so the LAST one added is the first consulted:
IApprover chain =
new TeamLeadApprover(
new ManagerApprover(
new DirectorApprover(
new BoardApprover())));
// The framework version of the same shape:
//
// app.Use(async (ctx, next) => { if (!Authorised(ctx)) { ctx.Response.StatusCode = 403; return; }
// await next(); });
//
// Returning without awaiting next() short-circuits the pipeline — that IS
// "this one is mine", and forgetting the await is the classic pipeline bug.
// Express middleware is a chain of responsibility that most JavaScript
// developers use daily without naming it: each function either responds
// (handling the request) or calls next() to pass it along.
const chain = [
{ name: 'team lead', handles: e => e.amount <= 500 },
{ name: 'manager', handles: e => e.amount <= 5_000 },
{ name: 'procurement', handles: e => e.vendorIsNew },
{ name: 'director', handles: e => e.amount <= 50_000 },
{ name: 'board', handles: () => true }, // terminator
]
export const review = expense => {
for (const link of chain) {
if (link.handles(expense)) return { approvedBy: link.name, expense }
}
throw new NoApproverFound(expense)
}
// The linked version, when each handler needs its own state or async work:
const link = (handles, approve, next) => async expense =>
handles(expense) ? approve(expense) : next(expense)
// DOM event bubbling is the browser's own chain: an event offered to the
// target, then to each ancestor in turn, until something calls
// stopPropagation() — which is precisely "this one is mine".
package approvals
// The idiomatic Go chain is a slice, because there is no inheritance to build a
// linked list with and no reason to want one.
type Approver struct {
Name string
Handles func(Expense) bool
Approve func(Expense) Decision
}
func Review(e Expense, chain []Approver) (Decision, error) {
for _, a := range chain {
if a.Handles(e) {
return a.Approve(e), nil
}
}
return Decision{}, fmt.Errorf("no approver for %s: %w", e.ID, ErrUnhandled)
}
// The http.Handler middleware form, where every link wraps the next:
//
// func RequireAuth(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// if !authorised(r) {
// http.Error(w, "forbidden", http.StatusForbidden)
// return // ← handled here; the chain stops
// }
// next.ServeHTTP(w, r)
// })
// }
//
// Note the two shapes differ in an important way: the slice version returns an
// ERROR when nothing matches, so an unhandled request cannot be silent. The
// middleware version can silently do nothing if a handler forgets to write a
// response — the same trap as the missing terminator.
type Decision = { approvedBy: string; expense: Expense }
interface Link {
readonly name: string
handles(expense: Expense): boolean
approve(expense: Expense): Decision
}
const chain: readonly Link[] = [
{ name: 'team lead', handles: e => e.amount <= 500, approve: e => ({ approvedBy: 'team lead', expense: e }) },
{ name: 'manager', handles: e => e.amount <= 5_000, approve: e => ({ approvedBy: 'manager', expense: e }) },
{ name: 'board', handles: () => true, approve: e => ({ approvedBy: 'board', expense: e }) },
]
export function review(expense: Expense): Decision {
for (const link of chain) {
if (link.handles(expense)) return link.approve(expense)
}
throw new NoApproverFound(expense)
}
// The type system can close the hole the pattern is famous for. Declare the
// return as Decision (not Decision | undefined) and the compiler will not let
// `review` fall off the end without throwing — so "nobody handled it" becomes a
// compile error rather than a silent afternoon.
//
// If a chain genuinely may not handle something, say so in the type and force
// every caller to deal with it:
// function review(e: Expense): Decision | Unhandled
Read across the tabs: notice how few of these use GoF's linked list. Python, Go, TypeScript and modern Java all reach for a list plus a loop, because the order then exists as data you can print, test and reorder from configuration, instead of being smeared across a set of next fields. The frameworks you already use are the linked form — Express and ASP.NET Core middleware, servlet filters, DOM event bubbling — and they share the same weak spot: a handler that forgets to call next(), or a chain with no terminator, fails by doing nothing at all.
05
How to implement Chain of Responsibility
Define a handler interface with one method that takes the request — and, if the chain is linked, a reference to the next handler.
Give each handler a single, narrow "is this mine?" test. A handler asking two unrelated questions should be two handlers.
Decide the order deliberately and write down why. Order is policy here, and nothing in the type system protects it.
Terminate the chain: either a default handler that always accepts, or an explicit error when the end is reached. Silence is not an option.
Assemble the chain in one place — a configuration file, a composition root, a factory — never scattered across the handlers themselves.
Log which handler took the request. Without that line, debugging a chain means reading every link and guessing.
Prefer a list and a loop over a linked list unless handlers genuinely need to wrap each other's execution.
06
When to use Chain of Responsibility — and when not to
Use it when more than one object can handle a request and the handler isn't known until runtime; when the set of handlers should be configurable or extensible without touching the sender; or when you want to issue a request to one of several objects without naming the receiver explicitly. Middleware, filters, validation pipelines and event bubbling are all this pattern.
Where it goes wrong
The silent drop. A request reaches the end and nothing happens — no exception, no log, no response. Whatever the domain, this is the failure mode people remember, and it is entirely preventable with a terminating handler.
Order is invisible policy. Swapping two links changes behaviour and breaks nothing that the compiler or the type system can see. Keep the order in one readable place and test it.
Debugging by archaeology. "Who approved this?" means reading every handler and mentally replaying the request. Log the handler that accepted, always.
Chains that do too much. If every handler runs and none stops the chain, you don't have Chain of Responsibility — you have a pipeline, and a plain list with a forEach says so more clearly than a pattern name.
Long chains, hot paths. Each link is a call and a predicate. Forty handlers on every request is measurable; keep the chain short or index it by request type.
You want to…
Use
Because
Give several objects a chance, until one handles it
Chain of Responsibility
The sender doesn't know who will take it, and one handler stops the chain.
Wrap an object so behaviour is added but always delegated
If the sender knows which handler it wants, a map beats a chain.
07
Quick check
🧠 Quick check
Your chain is TeamLead (≤£500) → Manager (≤£5,000) → Director (≤£50,000). Someone submits £120,000. What happens?
This is the pattern's signature bug. Every handler correctly decides "not mine" and passes it on; the last one has no next, and the default implementation of "pass it on" is to do nothing at all. No exception, no log, no rejection — the expense simply never gets approved, and nobody finds out until someone asks a month later. Two fixes, and you should pick one deliberately: end the chain with a handler whose test is always true (even if all it does is raise a ticket for a human), or make reaching the end throw. In TypeScript you can go further and let the return type make the silent path a compile error.
In the wild
JavaThe servlet FilterChain and Spring Security's filter chain; also java.util.logging, where a record walks up the parent loggers until propagate stops it.
C#The ASP.NET Core middleware pipeline — app.Use(async (ctx, next) => …) is a chain link, and returning without awaiting next() is "this one is mine".
JavaScriptExpress and Koa middleware, and DOM event bubbling — an event offered to each ancestor in turn until something calls stopPropagation().
Gonet/http middleware: func(http.Handler) http.Handler, where a link that writes a response and returns early stops the chain.
PythonDjango's middleware stack, and logging propagation from a logger to its parents — the usual cause of duplicated log lines.
C++Exception handling itself: a throw walks up the stack offering the exception to each enclosing catch until one matches. Qt propagates unhandled events up the widget parent chain the same way.
What is the difference between Chain of Responsibility and Decorator?
They have almost the same structure — an object holding the next object — and opposite contracts. A Decorator always passes the call along and adds something around it; removing one changes behaviour but never stops the call. A Chain of Responsibility link may consume the request and stop the chain entirely, so whether later links run at all depends on the data. If every link always runs, you have a decorator stack or a pipeline, not a chain.
What happens if no handler in the chain handles the request?
By default: nothing, silently — and that's the pattern's most notorious failure. GoF explicitly notes that receipt isn't guaranteed. In production you should always close the hole one of two ways: end the chain with a handler whose condition is always true (even if it just escalates or logs), or make reaching the end raise an error. A statically typed language can help — declare the return type as non-optional and the compiler forces you to handle the fall-through case.
Is middleware the Chain of Responsibility pattern?
Yes, with one twist. Express, Koa, Django and ASP.NET Core middleware are all chains where each component may handle the request or pass it on. The twist is that most middleware components don't stop the chain — they add a header, start a timer, attach a user — so a typical pipeline behaves more like a stack of decorators, with the occasional link (auth, rate limiting) that genuinely short-circuits. Both readings are useful; the short-circuit is what makes it a chain.
Should the chain be a linked list or an array?
An array or list, unless handlers genuinely need to wrap each other's execution (start a timer, open a transaction, then continue). A list keeps the order visible in one place you can print, test and load from configuration, and it removes a whole class of wiring bugs where a next pointer is set wrong or forgotten. The linked form earns its keep when a handler must do work both before and after the rest of the chain — which is exactly what middleware does.
Does the order of handlers matter?
Enormously, and nothing in the type system will tell you when it's wrong. A £200 order from a new vendor is approved by the team lead if the lead comes first, and routed to procurement if procurement does — both chains compile, both run, and they enforce different company policy. Keep the order defined in one readable place, write a test per ordering rule you care about, and log which handler accepted so a wrong order shows up in the data rather than in an audit.
Finish the handbook, earn the certificate
All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.