Beginner12 min readDesign Patternslive prototype

Chain of Responsibility

Pass a request along a chain of handlers, where each handler either deals with it or hands it to the next one. Think of a customer-support hotline: a password reset stops at Level 1, a $5,000 refund climbs up to the Manager — and the caller never needs to know who will pick it up, they just dial one number.

The idea

What it is

Chain of Responsibility is a behavioral pattern with a one-sentence intent: pass a request along a chain of handlers, where each handler either handles it or passes it to the next — and the sender never knows (or cares) which one will handle it. Instead of one big if/else deciding who does what, you line up small handlers like links in a chain and drop the request in at the front.

Picture a customer-support hotline. You call one number. Your request lands at Level 1 Support first. If L1 can fix it (a password reset — done in a minute), it stops right there. If not, L1 passes it up: L2 Support, then the Manager, then the Director. A $5,000 refund travels up to the Manager; you, the caller, never dialed the Manager directly — you didn't even know a Manager existed. And if a request is so wild that nobody on the ladder can approve it, it falls off the end unhandled. That hotline is Chain of Responsibility.

The one sentence to remember

The sender talks to the first link only. Each handler makes one small decision — handle it, or pass it on — so the sender is fully decoupled from whoever actually does the work.

A request can fall off the end

Unlike a plain function call, a chain gives no guarantee anyone will handle the request. That's a feature (easy to say 'not my job') and a trap (silent drops). Always decide up front what happens at the end of the chain: a default handler, an error, or an explicit 'unhandled' result.

Mechanics

How it works

The moving parts

The pattern needs only two small ideas — a shared handler interface, and a next pointer that links handlers together:

  • A Handler interface — every handler exposes the same two methods: setNext(handler) to wire up the link after it, and handle(request) to receive a request.
  • Concrete handlers — each one knows one small rule ('I do password resets', 'I approve refunds up to $100'). In handle(), it either deals with the request or forwards it: return this.next.handle(request).
  • The sender — holds a reference to the first link only. It calls firstHandler.handle(request) and walks away. It has no idea how long the chain is or who's in it.
  • The end of the chain — the last handler has no next. If the request reaches it and still isn't handled, it goes unhandled — so you choose a fallback: a catch-all default handler, a logged warning, or a returned 'nobody could do this' value.
typescript
interface Handler {
  setNext(next: Handler): Handler;   // returns next, so calls can be chained
  handle(req: SupportRequest): string | null;
}

class L1Support implements Handler {
  private next: Handler | null = null;
  setNext(next: Handler) { this.next = next; return next; }

  handle(req: SupportRequest): string | null {
    if (req.kind === "password-reset") {
      return "L1 handled it";               // ✅ my job — stop here
    }
    return this.next ? this.next.handle(req) : null;  // → pass it on
  }
}

// Wiring the hotline — the sender only ever sees 'l1':
const l1 = new L1Support();
l1.setNext(new L2Support()).setNext(new Manager()).setNext(new Director());
l1.handle({ kind: "refund", amount: 5000 });   // travels up to the Manager

Two details matter more than they look. First, order matters: put the Director before L1 and every password reset lands on the Director's desk. Chains usually go cheapest-first or most-specific-first. Second, the chain is built at runtime — it's just objects pointing at each other. You can add a handler, remove one, or reorder the whole ladder while the program runs, and the sender's code doesn't change by a single character.

Two flavors of chain

In the classic flavor, exactly one handler handles the request and the chain stops there (our hotline). In the pipeline flavor, every handler gets a turn and each does a bit of work before passing it along — that's how HTTP middleware works. Both are chains; the difference is just whether handling stops the walk.

You already use this every day

  • HTTP middleware pipelines — Express's app.use(...) and ASP.NET Core's middleware are chains: each middleware handles the request (auth, logging, compression) and calls next(), or short-circuits with a response.
  • DOM event bubbling — a click on a button travels up parent elements until some listener handles it (or calls stopPropagation() — literally breaking the chain).
  • Logging levels — a log record passes through loggers/filters; each decides to write it, transform it, or hand it up the logger hierarchy.
  • GUI event propagation — desktop toolkits route a keypress from the focused widget up through its containers until something consumes it.
  • Servlet filters — Java's FilterChain passes each request through a configurable line of filters before the servlet sees it.

The common win in all of these is decoupling: the code that sends a request is completely separated from the code that handles it. The sender knows one interface and one first link — that's low coupling with each handler keeping one focused job, exactly the goal of [[coupling-and-cohesion]]. And because each link wraps a tiny decision, the pattern feels like a cousin of [[decorator]] — same linked shape, but a decorator always passes through and adds behavior, while a chain handler may stop the walk by handling the request.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

A customer-support hotline as a live chain: 🧑‍💻 L1 Support → 👩‍🔧 L2 Support → 👔 Manager → 🏢 Director, each with its own tiny rule (password resets, refunds ≤ $100, ≤ $2,000, ≤ $10,000). Pick a request chip — 🔑 reset password, 💳 refund $80, 💸 refund $1,500, 🏦 refund $9,000, or 🚀 refund $50,000 — and press ▶ Send. The request slides along the chain; each handler flashes amber → pass or green ✓ handled, and everything past the handler dims because it never even sees the request. The $50,000 one falls off the end in red: ✗ nobody handled it. Then hit ⏏ Remove L2 and resend 💳 refund $80 — the same request now stops at the Manager instead. That's the whole pattern: handlers linked at runtime, each one deciding handle it or pass it on.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Send the easy ones

In the prototype, the chain reads 🧑‍💻 L1 Support → 👩‍🔧 L2 Support → 👔 Manager → 🏢 Director, each card showing its one-line rule. Pick the 🔑 reset password chip and press ▶ Send — L1 flashes green '✓ handled' immediately and everything after it dims (those handlers never even saw the request). Now send 💳 refund $80: L1 passes (amber '→ pass'), L2 handles. Watch the 'handlers asked' counter — 1, then 2.

try 02

Watch a request climb — and one fall off the end

Send 💸 refund $1,500: it passes L1 and L2 and stops at the Manager (handlers asked: 3). Send 🏦 refund $9,000: it climbs all the way to the Director. Now send 🚀 refund $50,000 — every card flashes '→ pass' and the request falls off the end in red: '✗ nobody handled it'. That's the pattern's honest edge case: a chain gives no guarantee of handling, so real systems add a fallback.

try 03

Re-wire the chain at runtime

Press ⏏ Remove L2 to unplug L2 Support, then resend 💳 refund $80. The same request now skips straight from L1 to the Manager, who handles it — a different handler, and the sender's click didn't change at all. That is runtime re-wiring: the chain is just objects linked by 'next' pointers, so you can add, remove, or reorder handlers while the program runs. Re-add L2 and ↺ Reset when done.

In practice

When to use it — and what trips people up

Reach for a chain when the handler isn't known up front

Chain of Responsibility earns its keep when who handles the request is a runtime question, not a compile-time one:

  • More than one possible handler, decided at runtime — different requests need different levels of authority or expertise, and you can't (or don't want to) hard-code the routing in one big if/else.
  • The sender must not know the receiver — you want to add a new approval level or a new middleware without touching a single line of sender code.
  • Handlers should be added, removed, or reordered freely — plugin filters, configurable pipelines, escalation ladders that change per customer tier. The chain is just linked objects, so re-wiring is cheap and live.
  • Processing in a specific order matters — auth before logging before compression: the chain makes the order explicit and swappable in one place (the wiring).

When NOT to use it

If there is exactly one fixed handler, just call it — a chain of one is an if statement wearing a costume. Same if every request must be handled by a known receiver: a direct call gives you a compile-time guarantee the chain can't. Add the chain when the set of handlers is genuinely open or the routing genuinely varies.

What it gives you

  • Decouples sender from receiver — the sender knows one interface and the first link; handlers can change completely behind it.
  • Single-responsibility handlers — each link holds one small rule (one approval limit, one filter), which is far easier to test than a monolithic if/else ladder.
  • Open for extension — add, remove, or reorder handlers at runtime without touching the sender or the other handlers (Open/Closed in action).
  • Order is explicit and controllable — the wiring code is the one place that states processing order, instead of it being implied by tangled conditionals.

Common mistakes

  • No guarantee of handling — a request can silently fall off the end of the chain, so you must design a default/fallback or an explicit 'unhandled' signal.
  • Harder to debug and trace — the actual handler is only known at runtime; following a request means stepping link by link through the chain.
  • Latency through long chains — every request walks past each non-handling link, which adds calls (and in middleware pipelines, real per-request cost).
  • Misconfigured wiring is a runtime bug — wrong order or a forgotten setNext() breaks routing, and the compiler can't catch it for you.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

type SupportRequest =
  | { kind: "password-reset" }
  | { kind: "refund"; amount: number };

interface Handler {
  setNext(next: Handler): Handler;         // returns next → fluent wiring
  handle(req: SupportRequest): string;
}

// Base link: stores 'next' and forwards by default.
abstract class SupportHandler implements Handler {
  private next: Handler | null = null;
  setNext(next: Handler): Handler { this.next = next; return next; }

  handle(req: SupportRequest): string {
    if (this.canHandle(req)) return `${this.name} handled it ✓`;
    if (this.next) return this.next.handle(req);   // → pass it on
    return "✗ nobody handled it";                  // fell off the end
  }

  protected abstract name: string;
  protected abstract canHandle(req: SupportRequest): boolean;
}

class L1Support extends SupportHandler {
  protected name = "L1 Support";
  protected canHandle(r: SupportRequest) { return r.kind === "password-reset"; }
}
class L2Support extends SupportHandler {
  protected name = "L2 Support";
  protected canHandle(r: SupportRequest) { return r.kind === "refund" && r.amount <= 100; }
}
class Manager extends SupportHandler {
  protected name = "Manager";
  protected canHandle(r: SupportRequest) { return r.kind === "refund" && r.amount <= 2000; }
}
class Director extends SupportHandler {
  protected name = "Director";
  protected canHandle(r: SupportRequest) { return r.kind === "refund" && r.amount <= 10000; }
}

// Wire the hotline. The caller only ever knows 'hotline' (the first link).
const hotline = new L1Support();
hotline.setNext(new L2Support()).setNext(new Manager()).setNext(new Director());

console.log(hotline.handle({ kind: "password-reset" }));       // L1 Support handled it ✓
console.log(hotline.handle({ kind: "refund", amount: 5000 })); // Director handled it ✓
console.log(hotline.handle({ kind: "refund", amount: 50000 }));// ✗ nobody handled it

References & further reading

5 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What is the core intent of the Chain of Responsibility pattern?

question 02 / 05

In the support-hotline chain L1 → L2 (refunds ≤ $100) → Manager (≤ $2,000) → Director (≤ $10,000), who handles a $1,500 refund?

question 03 / 05

How much does the sender know about the chain?

question 04 / 05

What happens if no handler in the chain can handle a request (like the $50,000 refund)?

question 05 / 05

When is Chain of Responsibility the WRONG choice?

0/5 answered