Free Interactive Course · Design Patterns

Chain of Responsibility Design Pattern

Pass a request along a line of handlers until one of them takes it — and decide, deliberately, what happens if none do.

Behavioural Patternsmediuma.k.a. Handler Chaina.k.a. Pipeline
ShareXLinkedIn
In one sentence

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:

  1. Every handler implements the same interface and gets a reference to the next handler in the chain.
  2. 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.
  3. The sender knows only the first handler. It has no idea how long the chain is or who ends up approving.
  4. 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.
ExpenseFormthe senderTeamLead≤ £500Manager≤ £5,000Director≤ £50,000Boardalways acceptsnot mine → pass it onnot mine → pass it onsubmit(£12,400) → lead passes → manager passes → director approvesthe sender knows only the first link — and the green box is why nothing falls off the end
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.
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

  1. Define a handler interface with one method that takes the request — and, if the chain is linked, a reference to the next handler.
  2. Give each handler a single, narrow "is this mine?" test. A handler asking two unrelated questions should be two handlers.
  3. Decide the order deliberately and write down why. Order is policy here, and nothing in the type system protects it.
  4. Terminate the chain: either a default handler that always accepts, or an explicit error when the end is reached. Silence is not an option.
  5. Assemble the chain in one place — a configuration file, a composition root, a factory — never scattered across the handlers themselves.
  6. Log which handler took the request. Without that line, debugging a chain means reading every link and guessing.
  7. 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…UseBecause
Give several objects a chance, until one handles itChain of ResponsibilityThe sender doesn't know who will take it, and one handler stops the chain.
Wrap an object so behaviour is added but always delegatedDecoratorA Decorator always calls the inner object; a chain link may stop.
Turn the request itself into an object you can queue or undoCommandCommand is about the request; CoR is about who receives it.
Route between many objects that all know a hubMediatorA Mediator centralises the routing; a chain distributes it.
Pick one handler by a known keyStrategyIf 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?

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.

Frequently asked questions

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.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.