codejump

AcademyDesign Patterns

Chain of Responsibility pattern

Trying handlers one at a time until one accepts the request, the dead end when a request reaches the last link unhandled, and why every chain needs a terminal case.

7 min readUpdated

3 exercises

Waiting at the end of this page.

Chain of Responsibility decides who handles a request without any single object knowing about every possible handler: candidates are tried one at a time, in a line, until one accepts.

Where Mediator centralizes communication through one hub that knows about everyone, Chain of Responsibility decentralizes it into a line where each link only has to know the next one in it. A request travels down the chain until a handler that can deal with it says so; every handler that can't just passes it along.

The problem it solves

A customer pays for an order of 210. They have two accounts, and you want to charge the first one that can cover it. A support ticket might be handled by a bot, then a junior agent, then a manager. A request to your API needs authentication, validation and rate limiting before the real handler runs.

The naive version puts every rule in one place:

TypeScript
if (a.balance >= amount) return payWith(a);
if (b.balance >= amount) return payWith(b);
if (c.balance >= amount) return payWith(c);
  • The sender knows every handler. Adding a fourth means editing the sender.
  • The order is baked in. Reordering or skipping steps for one caller means another copy of the code.
  • Handlers cannot be reused. Each rule is tangled with the code that calls it.

The fix is to give each rule its own object with a link to the next one. The sender hands the request to the first link and stops caring.

Structure

Each handler either deals with the request or passes it to the next link. The sender only knows the first one
Each handler either deals with the request or passes it to the next link. The sender only knows the first one

Three parts:

  • A handler interface with a handle(request) method and a way to set the next handler.
  • Concrete handlers, each deciding: handle it, or forward it.
  • The client, which builds the chain and sends the request into the first link.
class Handler {
  setNext(handler) { this.next = handler; return this; }
  handle(amount) {
    if (this.next) return this.next.handle(amount);
    return 'no handler could cover ' + amount;
  }
}

class AccountA extends Handler {
  constructor(balance) { super(); this.balance = balance; }
  handle(amount) {
    if (amount <= this.balance) return 'paid by A';
    return super.handle(amount);
  }
}
class AccountB extends Handler {
  constructor(balance) { super(); this.balance = balance; }
  handle(amount) {
    if (amount <= this.balance) return 'paid by B';
    return super.handle(amount);
  }
}

// const chain = new AccountA(100).setNext(new AccountB(300));
// chain.handle(210) → 'paid by B' — A can't cover it, forwards to B

Account A cannot cover 210, so it forwards the request. Account B can, so it handles it and the chain stops there. A request nobody can handle falls off the end of the chain, so the last link has to decide what that means: an error, or a default.

Where it is used in real systems

A request passes through a line of checks. Any link can stop it early with its own response
A request passes through a line of checks. Any link can stop it early with its own response
Use caseThe chain
HTTP middlewareAuthentication, validation, rate limiting, then the route handler. Any step can reject the request.
Support escalationBot, level 1, level 2, manager. Each takes what it can and escalates the rest.
Payment routingTry the primary card, then the backup, then the wallet balance.
Event handling in UIsA click bubbles from the target element up through its ancestors until someone handles it.
Approval workflowsTeam lead, director, VP, based on the amount.
Logging levelsEach logger handles the levels it cares about and forwards the rest.
Validation pipelinesA series of rules, stopping at the first failure.

The signal to look for: several candidates, a fixed order, and the first one that can deal with it wins (or every one gets a turn).

Chains you already use

  • Express, Koa and Fastify middleware. next() is the link to the next handler, and a middleware that sends a response without calling next() ends the chain.
  • DOM event bubbling. An event travels up from the clicked element through its parents, and stopPropagation() ends the chain.
  • Redux middleware and Axios interceptors. Actions and requests pass through a line of functions.
  • `try` and `catch` across the call stack. An exception travels up until some caller handles it.
  • NestJS guards, pipes and interceptors. Each runs in a defined order, and any of them can stop the request.

Chain of Responsibility or Decorator

They look alike, which is why interviewers ask.

Chain of ResponsibilityDecorator
Each linkMay stop the requestAlways forwards, adding behavior
ResultOne handler usually handles itEvery layer contributes
IntentFind who can handle itAdd behavior around it

Express middleware sits in between: many middlewares always call next() (decorator-like), and some end the chain (Chain of Responsibility).

When not to use it

  • Nobody is guaranteed to handle the request. Without a fallback at the end, requests can vanish silently. Always decide what happens at the end of the chain.
  • The order is hard to see. A chain assembled across many files is hard to debug. Build it in one place and name it.
  • You need to know who handled it. A chain hides that by design. If callers need the handler's identity, a plain lookup is clearer.
  • A simple `if` is enough. Two fixed checks in one function do not need a chain.
  • Performance-sensitive paths. Every link adds a call, and a long chain adds up.

What interviewers ask

  • "What is Chain of Responsibility?" Passing a request along a line of handlers until one handles it.
  • "What is the benefit?" The sender is decoupled from the handlers, and handlers can be added, removed or reordered without touching it.
  • "Give a real example." Express middleware, or DOM event bubbling.
  • "What if no handler handles the request?" It falls off the end. Provide a default or throw a clear error.
  • "Chain of Responsibility versus Decorator?" Chain links may stop the request. Decorators always forward and add behavior.

The mistake to avoid

A chain where a handler that cannot handle the request forgets to pass it on. In the example, AccountA calls super.handle(amount) when it cannot pay. Drop that line and the request dies at the first link, and the caller gets undefined with no error. Have every handler either handle the request or forward it, never neither.

The mechanism above, an object holding a reference to the next one and delegating to it, is the same in JavaScript, Python or Java. Only the syntax changes.

Now practice it

1 challenge · 2 quizzes

Reading this page is the cheap half. These are the exercises that make you use it.