Chain of Responsibility
Chain of Responsibility is a behavioral pattern that routes a request through a series of independent handlers, each free to process the request itself or hand it off to the next one in line.
Problem
Picture a support desk where every incoming ticket has to be routed based on its complexity: a bot answers FAQ-style questions, a first-line agent handles common account issues, and only hard technical cases reach an engineer.
A straightforward implementation stuffs all the routing logic into one dispatcher function full of nested conditionals - one branch per tier. The moment someone wants to insert a new tier (say, a billing specialist between the agent and the engineer), they have to touch that same fragile function and re-verify every existing branch still works.
Worse, the dispatcher ends up permanently coupled to every tier it routes to, even though each tier only cares about its own slice of the work.
Solution
Chain of Responsibility replaces the monolithic dispatcher with a sequence of small handler objects, each aware only of the next handler in line. When a request arrives at a handler, it inspects the request and either deals with it on the spot or forwards it unchanged to its successor.
Because every handler implements the same interface, the caller never needs to know which concrete handler will end up processing the request - it just hands the request to the first link and lets the chain sort itself out. New tiers are added by writing one more handler class and slotting it into the chain; nothing else in the system needs to change, and the chain itself can be assembled or rearranged at runtime.
When to Use
- Reach for this pattern when a request might need one of several different treatments and you can't pin down in advance which handler, or how many, will actually run.
- Use it when several independent steps must run against the same request in a specific, well-defined order.
- It fits well when the roster of handlers - or their order - needs to change while the application is running, not just at compile time.
Real-World Examples
- **HTTP middleware stacks** - frameworks like Express and Koa pass every incoming request through a sequence of middleware functions, each free to respond, mutate the request, or call `next()`.
- **DOM event propagation** - a click or keypress bubbles up from the target element through each ancestor, and any listener along the way can call `stopPropagation()` to end the journey.
- **Logging framework filters** - libraries like Log4j or Python's `logging` route a log record through a chain of filters and handlers, each deciding whether to act on it or pass it along.
Structure
A common Handler interface exposes the method every link must implement, plus a way to point at a successor. An optional base class factors out the bookkeeping - storing the successor reference and forwarding by default - so concrete handlers only need to write the logic specific to what they check. The client wires the handlers together into a chain and always starts by sending the request to the first one.
Participants
Defines the contract every link in the chain must follow: a method to handle (or forward) a request, and usually a method to attach the next link.
An optional convenience class that keeps the successor reference and default pass-through logic in one place, so concrete handlers don't repeat that plumbing.
Implements the actual check for one specific case. Either resolves the request itself or delegates to whatever comes next in the chain.
In the diagram: Team Lead, Manager, Director, CEO.
Assembles the handlers into a chain (in whatever order the use case requires) and submits requests only to the first handler, never to the ones further down.
In the diagram: the requester submitting the $50,000 expense.
class Approver {
constructor(name, limit) {
this.name = name;
this.limit = limit;
this.next = null;
}
setNext(handler) {
this.next = handler;
return handler;
}
approve(amount) {
if (amount <= this.limit) {
console.log(`${this.name} approved $${amount}`);
} else if (this.next) {
this.next.approve(amount);
} else {
console.log(`No one can approve $${amount}`);
}
}
}
class TeamLead extends Approver { constructor() { super('Team Lead', 1000); } }
class Manager extends Approver { constructor() { super('Manager', 10000); } }
class Director extends Approver { constructor() { super('Director', 100000); } }
class Ceo extends Approver { constructor() { super('CEO', Infinity); } }
const lead = new TeamLead();
const manager = new Manager();
const director = new Director();
const ceo = new Ceo();
lead.setNext(manager).setNext(director).setNext(ceo);
lead.approve(800);
lead.approve(5000);
lead.approve(90000);
lead.approve(500000);
Step 1 of 6
Approver stores its own limit and a link to the next handler
The constructor takes `name` and `limit`, and initializes `this.next = null` - every handler in the chain is just an object with a threshold and an optional successor.
Step 2 of 6
setNext links handlers and returns the argument for chaining
`setNext(handler)` stores `handler` in `this.next` and returns that same `handler`, not `this` - that's what lets calls be chained fluently like `a.setNext(b).setNext(c)`.
Step 3 of 6
approve handles it locally or forwards down the chain
If `amount <= this.limit` the handler approves it itself; otherwise, if `this.next` exists, it delegates by calling `this.next.approve(amount)`; if there's no next handler left, it logs that no one could approve it.
Step 4 of 6
Four concrete handlers just fix a name and a limit
`TeamLead`, `Manager`, `Director` and `Ceo` each `extends Approver` and call `super(name, limit)` with an increasing threshold, up to `Infinity` for the CEO - none of them override `approve` or `setNext`.
Step 5 of 6
The chain is wired together with one fluent statement
`lead.setNext(manager).setNext(director).setNext(ceo)` works because each `setNext` returns its argument, so the next `.setNext(...)` is called on the handler that was just linked in.
Step 6 of 6
The client only ever talks to the first handler
Every call goes through `lead.approve(...)` - the caller doesn't know or care which handler in the chain ends up approving each amount.
interface Handler {
setNext(handler: Handler): Handler;
approve(amount: number): void;
}
abstract class Approver implements Handler {
private next: Handler | null = null;
constructor(
protected readonly name: string,
protected readonly limit: number,
) {}
setNext(handler: Handler): Handler {
this.next = handler;
return handler;
}
approve(amount: number): void {
if (amount <= this.limit) {
console.log(`${this.name} approved $${amount}`);
} else if (this.next) {
this.next.approve(amount);
} else {
console.log(`No one can approve $${amount}`);
}
}
}
class TeamLead extends Approver { constructor() { super('Team Lead', 1_000); } }
class Manager extends Approver { constructor() { super('Manager', 10_000); } }
class Director extends Approver { constructor() { super('Director', 100_000); } }
class Ceo extends Approver { constructor() { super('CEO', Infinity); } }
const lead = new TeamLead();
const manager = new Manager();
const director = new Director();
const ceo = new Ceo();
lead.setNext(manager).setNext(director).setNext(ceo);
lead.approve(800);
lead.approve(5_000);
lead.approve(90_000);
lead.approve(500_000);
abstract class Approver {
protected final String name;
protected final int limit;
private Approver next;
protected Approver(String name, int limit) {
this.name = name;
this.limit = limit;
}
public Approver setNext(Approver handler) {
this.next = handler;
return handler;
}
public void approve(int amount) {
if (amount <= limit) {
System.out.printf("%s approved $%d%n", name, amount);
} else if (next != null) {
next.approve(amount);
} else {
System.out.printf("No one can approve $%d%n", amount);
}
}
}
class TeamLead extends Approver { TeamLead() { super("Team Lead", 1_000); } }
class Manager extends Approver { Manager() { super("Manager", 10_000); } }
class Director extends Approver { Director() { super("Director", 100_000); } }
class Ceo extends Approver { Ceo() { super("CEO", Integer.MAX_VALUE); } }
Approver lead = new TeamLead();
Approver manager = new Manager();
Approver director = new Director();
Approver ceo = new Ceo();
lead.setNext(manager).setNext(director).setNext(ceo);
lead.approve(800);
lead.approve(5_000);
lead.approve(90_000);
lead.approve(500_000);
abstract class Approver
{
protected readonly string Name;
protected readonly int Limit;
private Approver _next;
protected Approver(string name, int limit) { Name = name; Limit = limit; }
public Approver SetNext(Approver handler) { _next = handler; return handler; }
public void Approve(int amount)
{
if (amount <= Limit)
Console.WriteLine($"{Name} approved ${amount}");
else if (_next != null)
_next.Approve(amount);
else
Console.WriteLine($"No one can approve ${amount}");
}
}
class TeamLead : Approver { public TeamLead() : base("Team Lead", 1_000) {} }
class Manager : Approver { public Manager() : base("Manager", 10_000) {} }
class Director : Approver { public Director() : base("Director", 100_000) {} }
class Ceo : Approver { public Ceo() : base("CEO", int.MaxValue) {} }
var lead = new TeamLead();
var manager = new Manager();
var director = new Director();
var ceo = new Ceo();
lead.SetNext(manager).SetNext(director).SetNext(ceo);
lead.Approve(800);
lead.Approve(5_000);
lead.Approve(90_000);
lead.Approve(500_000);
from __future__ import annotations
from abc import ABC
class Approver(ABC):
def __init__(self, name: str, limit: int) -> None:
self.name = name
self.limit = limit
self._next: Approver | None = None
def set_next(self, handler: Approver) -> Approver:
self._next = handler
return handler
def approve(self, amount: int) -> None:
if amount <= self.limit:
print(f'{self.name} approved ${amount}')
elif self._next:
self._next.approve(amount)
else:
print(f'No one can approve ${amount}')
class TeamLead(Approver):
def __init__(self) -> None:
super().__init__('Team Lead', 1_000)
class Manager(Approver):
def __init__(self) -> None:
super().__init__('Manager', 10_000)
class Director(Approver):
def __init__(self) -> None:
super().__init__('Director', 100_000)
class Ceo(Approver):
def __init__(self) -> None:
super().__init__('CEO', float('inf'))
lead = TeamLead()
manager = Manager()
director = Director()
ceo = Ceo()
lead.set_next(manager).set_next(director).set_next(ceo)
lead.approve(800)
lead.approve(5_000)
lead.approve(90_000)
lead.approve(500_000)
Step 1 of 6
Approver stores its own limit and a link to the next handler
`__init__` takes `name` and `limit`, and sets `self._next: Approver | None = None` - every handler is just an object with a threshold and an optional successor.
Step 2 of 6
set_next links handlers and returns the argument for chaining
`set_next(handler)` stores `handler` in `self._next` and returns that same `handler`, not `self` - that's what lets calls be chained fluently like `a.set_next(b).set_next(c)`.
Step 3 of 6
approve handles it locally or forwards down the chain
If `amount <= self.limit` the handler prints its own approval; otherwise, if `self._next` is set, it delegates via `self._next.approve(amount)`; if not, it prints that no one could approve it.
Step 4 of 6
Four concrete handlers just fix a name and a limit
`TeamLead`, `Manager`, `Director` and `Ceo` each subclass `Approver` and call `super().__init__(name, limit)` with an increasing threshold, up to `float('inf')` for the CEO - none of them override `approve` or `set_next`.
Step 5 of 6
The chain is wired together with one fluent statement
`lead.set_next(manager).set_next(director).set_next(ceo)` works because each `set_next` returns its argument, so the next `.set_next(...)` is called on the handler that was just linked in.
Step 6 of 6
The client only ever talks to the first handler
Every call goes through `lead.approve(...)` - the caller doesn't know or care which handler in the chain ends up approving each amount.
Click "Run" to execute this code in a sandboxed frame and see console output here.
TypeScript runs as plain JavaScript here - type annotations are stripped, not type-checked.
Click "Run" to execute this code in a sandboxed frame and see console output here.
Advantages
- Callers stay ignorant of which handler ultimately does the work, so the code that issues a request and the code that fulfills it can evolve independently.
- New handling steps are added by writing a new class and splicing it into the chain - nothing that already works has to be edited.
- The order in which handlers get a chance to act is explicit and fully under your control, since it's just the order they're linked in.
- Chains can be assembled differently for different scenarios, or rebuilt on the fly, without touching the handler classes themselves.
Disadvantages
- If every handler declines and nobody remembered to add a catch-all at the end, a request can silently fall off the chain unhandled.
- Tracing why a particular handler ended up processing (or not processing) a request means stepping through every link that came before it.
- A very long chain adds a small but real overhead, since a request may pass through several handlers that ultimately do nothing with it.
Question 1 of 10
What does Chain of Responsibility do with an incoming request?
Correct! Chain of Responsibility routes a request through a series of independent handlers, each free to process it itself or hand it off to the next one in line.
Not quite. Chain of Responsibility routes a request through a series of independent handlers, each free to process it itself or hand it off to the next one in line.
Question 2 of 10
In the expense-approval structure, what role does the optional BaseHandler class play?
Correct! An optional base class factors out the bookkeeping - storing the successor reference and forwarding by default - so concrete handlers only need to write the logic specific to what they check.
Not quite. An optional base class factors out the bookkeeping - storing the successor reference and forwarding by default - so concrete handlers only need to write the logic specific to what they check.
Question 3 of 10
In the diagram walkthrough for a $50,000 expense, why does Director handle the request instead of the CEO?
Correct! The request moves forward until one handler accepts it, then it stops - Director's limit covers $50,000, so the journey ends there and the CEO is never reached.
Not quite. The request moves forward until one handler accepts it, then it stops - Director's limit covers $50,000, so the journey ends there and the CEO is never reached.
Question 4 of 10
In the JavaScript implementation, why does setNext return the handler it was given rather than this?
Correct! setNext(handler) stores handler in this.next and returns that same handler, not this - that's what lets calls be chained fluently like a.setNext(b).setNext(c).
Not quite. setNext(handler) stores handler in this.next and returns that same handler, not this - that's what lets calls be chained fluently like a.setNext(b).setNext(c).
Question 5 of 10
In the support-desk example, why does a single dispatcher function full of nested conditionals become a problem?
Correct! Adding a new tier means touching the same fragile dispatcher function and re-verifying every existing branch, and the dispatcher ends up permanently coupled to every tier.
Not quite. Adding a new tier means touching the same fragile dispatcher function and re-verifying every existing branch, and the dispatcher ends up permanently coupled to every tier.
Question 6 of 10
What is a stated risk if no handler in the chain accepts a request?
Correct! If every handler declines and nobody remembered to add a catch-all at the end, a request can silently fall off the chain unhandled.
Not quite. If every handler declines and nobody remembered to add a catch-all at the end, a request can silently fall off the chain unhandled.
Question 7 of 10
What does a single handler do when a request arrives at it?
Correct! When a request arrives at a handler, it inspects the request and either deals with it on the spot or forwards it unchanged to its successor.
Not quite. When a request arrives at a handler, it inspects the request and either deals with it on the spot or forwards it unchanged to its successor.
Question 8 of 10
Why does tracing which handler processed a request get harder as the chain grows?
Correct! Tracing why a particular handler ended up processing (or not processing) a request means stepping through every link that came before it.
Not quite. Tracing why a particular handler ended up processing (or not processing) a request means stepping through every link that came before it.
Question 9 of 10
Which of these is listed as a genuine benefit of Chain of Responsibility?
Correct! Chains can be assembled differently for different scenarios, or rebuilt on the fly, without touching the handler classes themselves.
Not quite. Chains can be assembled differently for different scenarios, or rebuilt on the fly, without touching the handler classes themselves.
Question 10 of 10
Why doesn't the client need to know which concrete handler ends up processing a request?
Correct! Because every handler implements the same interface, the caller never needs to know which concrete handler will end up processing the request - it just hands the request to the first link and lets the chain sort itself out.
Not quite. Because every handler implements the same interface, the caller never needs to know which concrete handler will end up processing the request - it just hands the request to the first link and lets the chain sort itself out.