Chain of Responsibility
Chains handlers in a sequential line, giving each one the chance to process a request or pass it to the next — decoupling whoever sends the request from whoever processes it.
Intent
Avoid coupling between sender and receiver of a request, giving more than one object the opportunity to handle it. Handlers are chained; each one can process the request and stop the chain, or pass it to the next handler without the sender knowing how many handlers exist or which one will act.
Cataloged by the GoF (1994) as a behavioral pattern, Chain of
Responsibility (CoR) shows up in HTTP middleware pipelines
(authentication → authorization → throttle → route handler), in
hierarchical approval flows (employee → manager → director → CEO),
in logging systems (debug → info → warning → error), and in UI
event filters. The chain is assembled at runtime with
setNext(), which allows changing, reordering, or
extending the handlers without modifying the sender.
Problem
Imagine a corporate expense approval system. Different amounts require approval from different hierarchical levels. The naive approach centralizes all the logic in a single point:
// Naive approach — DON'T do this:
function approveExpense(amount: number, approver: string): boolean {
if (approver === 'lead' && amount <= 1000) {
console.log('Lead approved');
return true;
} else if (approver === 'manager' && amount <= 5000) {
console.log('Manager approved');
return true;
} else if (approver === 'director' && amount <= 20000) {
console.log('Director approved');
return true;
}
console.log('Rejected');
return false;
}
// Problem: the sender needs to know WHO should approve; adding a level
// requires modifying this function; conditionals grow indefinitely.
Immediate problems: the sender needs to know every possible approver and their rules; adding a new approval level requires modifying the central function; each level's logic is mixed into a single block; there's no way to reorder or replace an approver without changing the code that uses them.
Chain of Responsibility solves this by turning each approver into a
handler with the same interface, chained via setNext().
The sender sends to the first handler and doesn't know — and
doesn't need to know — who will process the request.
Solution
Chain of Responsibility organizes the code around two central elements:
-
Handler (interface/abstract class): declares the
handling method (e.g.:
handle(request)) and the chaining method (setNext(handler)). The abstract base class implementssetNext()and the default forwarding: if it doesn't process it, it delegates to the next. The concrete handler only needs to implement its own decision logic. -
ConcreteHandler: implements the check logic —
decides whether to process the request (and stop the chain) or
call
super.handle()to pass it to the next. E.g.:LeadHandlerthat approves only if the amount is ≤ 1000.
The chain is assembled by the client, chaining handlers with
setNext(). It usually returns this to
allow fluent chaining:
lead.setNext(manager).setNext(director).
Structure
«interface» Handler
┌────────────────────────────────────────┐
│ + setNext(h: Handler): Handler │
│ + handle(req: Request): string|null │
└────────────────────────────────────────┘
▲
HandlerBase (abstract class)
┌────────────────────────────────────────┐
│ - next: Handler | null │
│ + setNext(h: Handler): Handler │
│ → this.next = h; return h │
│ + handle(req): string|null │
│ → this.next?.handle(req) ?? null │
└────────────────────────────────────────┘
▲
┌───────────┴────────────┬──────────────────┐
│ │ │
LeadHandler ManagerHandler DirectorHandler
│ handle(req): │ handle(req): │ handle(req):
│ if amount <= 1000 │ if amount <= 5000│ if amount <= 20000
│ → processes │ → processes │ → processes
│ else │ else │ else
│ → super.handle(req)│ → super │ → super
Chain assembly (client):
lead.setNext(manager).setNext(director)
Flow for amount = 3500:
lead.handle(3500)
→ 3500 > 1000 → passes to manager
manager.handle(3500)
→ 3500 <= 5000 → MANAGER APPROVES ✓ (chain stops)
Flow for amount = 50000:
lead → manager → director → null (no handler) → rejected
Code examples
Example 1 — Expense approval pipeline
A complete implementation with the Handler interface,
an abstract base class that encapsulates forwarding, and concrete
handlers for each approval level. Note the fluent chaining via
setNext() returning the next handler.
// ── Handler interface ─────────────────────────────────────────
interface Handler {
setNext(handler: Handler): Handler;
handle(amount: number): string | null;
}
// ── Abstract base class — implements setNext and default forwarding ─
abstract class HandlerBase implements Handler {
private next: Handler | null = null;
setNext(handler: Handler): Handler {
this.next = handler;
return handler; // returns the next → fluent chaining
}
handle(amount: number): string | null {
// Default behavior: pass to the next or return null
return this.next ? this.next.handle(amount) : null;
}
}
// ── Concrete handlers ─────────────────────────────────────────
class LeadHandler extends HandlerBase {
handle(amount: number): string | null {
if (amount <= 1_000) {
return `Lead approved $${amount}`;
}
return super.handle(amount); // passes to the next
}
}
class ManagerHandler extends HandlerBase {
handle(amount: number): string | null {
if (amount <= 5_000) {
return `Manager approved $${amount}`;
}
return super.handle(amount);
}
}
class DirectorHandler extends HandlerBase {
handle(amount: number): string | null {
if (amount <= 20_000) {
return `Director approved $${amount}`;
}
return super.handle(amount);
}
}
// Final handler: catches whatever escaped the chain
class RejectedHandler extends HandlerBase {
handle(amount: number): string | null {
return `REJECTED — $${amount} exceeds the $20,000 limit`;
}
}
// ── Chain assembly ──────────────────────────────────────────────
const lead = new LeadHandler();
const manager = new ManagerHandler();
const director = new DirectorHandler();
const end = new RejectedHandler();
// Fluent chaining: lead → manager → director → end
lead.setNext(manager).setNext(director).setNext(end);
// ── Usage ────────────────────────────────────────────────────
const expenses = [500, 3_500, 15_000, 50_000];
for (const amount of expenses) {
const result = lead.handle(amount);
console.log(result);
}
// Lead approved $500
// Manager approved $3500
// Director approved $15000
// REJECTED — $50000 exceeds the $20,000 limit
<?php
// ── Handler interface ─────────────────────────────────────────
interface Handler
{
public function setNext(Handler $handler): Handler;
public function handle(int $amount): ?string;
}
// ── Abstract base class — implements setNext and default forwarding ─
abstract class HandlerBase implements Handler
{
private ?Handler $next = null;
public function setNext(Handler $handler): Handler
{
$this->next = $handler;
return $handler; // returns the next → fluent chaining
}
public function handle(int $amount): ?string
{
return $this->next?->handle($amount);
}
}
// ── Concrete handlers ─────────────────────────────────────────
class LeadHandler extends HandlerBase
{
public function handle(int $amount): ?string
{
if ($amount <= 1_000) {
return "Lead approved \${$amount}";
}
return parent::handle($amount);
}
}
class ManagerHandler extends HandlerBase
{
public function handle(int $amount): ?string
{
if ($amount <= 5_000) {
return "Manager approved \${$amount}";
}
return parent::handle($amount);
}
}
class DirectorHandler extends HandlerBase
{
public function handle(int $amount): ?string
{
if ($amount <= 20_000) {
return "Director approved \${$amount}";
}
return parent::handle($amount);
}
}
// Final handler: catches whatever escaped the chain
class RejectedHandler extends HandlerBase
{
public function handle(int $amount): ?string
{
return "REJECTED — \${$amount} exceeds the \$20,000 limit";
}
}
// ── Chain assembly ──────────────────────────────────────────────
$lead = new LeadHandler();
$manager = new ManagerHandler();
$director = new DirectorHandler();
$end = new RejectedHandler();
// Fluent chaining: lead → manager → director → end
$lead->setNext($manager)->setNext($director)->setNext($end);
// ── Usage ────────────────────────────────────────────────────
$expenses = [500, 3_500, 15_000, 50_000];
foreach ($expenses as $amount) {
echo $lead->handle($amount) . "\n";
}
// Lead approved $500
// Manager approved $3500
// Director approved $15000
// REJECTED — $50000 exceeds the $20,000 limit
Example 2 — HTTP middleware: authentication → authorization → throttle
The pattern is identical to that of HTTP frameworks like Express.js or Laravel. Each middleware decides to process (and pass to the next) or stop the chain by returning an error response. The chain is assembled in the application's configuration, independent of the routes.
// ── Simplified HTTP request/response types ────────────────────
interface Request {
path: string;
token?: string;
role?: 'admin' | 'user';
ip: string;
}
interface Response {
status: number;
body: string;
}
// ── Middleware interface ──────────────────────────────────────
interface Middleware {
setNext(m: Middleware): Middleware;
process(req: Request): Response;
}
// ── Abstract base class ───────────────────────────────────────
abstract class MiddlewareBase implements Middleware {
private next: Middleware | null = null;
setNext(m: Middleware): Middleware {
this.next = m;
return m;
}
protected forward(req: Request): Response {
return this.next
? this.next.process(req)
: { status: 204, body: 'OK (no final handler)' };
}
abstract process(req: Request): Response;
}
// ── Middleware 1: authentication ──────────────────────────────
class AuthenticationMiddleware extends MiddlewareBase {
process(req: Request): Response {
if (!req.token) {
return { status: 401, body: 'Not authenticated — missing token' };
}
console.log('[Auth] valid token');
return this.forward(req);
}
}
// ── Middleware 2: authorization ───────────────────────────────
class AuthorizationMiddleware extends MiddlewareBase {
constructor(private readonly requiredRole: 'admin' | 'user') {
super();
}
process(req: Request): Response {
if (req.role !== this.requiredRole && req.role !== 'admin') {
return { status: 403, body: `Access denied — requires role "${this.requiredRole}"` };
}
console.log(`[Authz] role "${req.role}" authorized`);
return this.forward(req);
}
}
// ── Middleware 3: throttle by IP ──────────────────────────────
class ThrottleMiddleware extends MiddlewareBase {
private readonly counters = new Map<string, number>();
private readonly limit: number;
constructor(requestsPerMinute: number) {
super();
this.limit = requestsPerMinute;
}
process(req: Request): Response {
const count = (this.counters.get(req.ip) ?? 0) + 1;
this.counters.set(req.ip, count);
if (count > this.limit) {
return { status: 429, body: `Limit exceeded for IP ${req.ip}` };
}
console.log(`[Throttle] ${req.ip}: ${count}/${this.limit}`);
return this.forward(req);
}
}
// ── Final handler: route ──────────────────────────────────────
class RouteHandler extends MiddlewareBase {
process(req: Request): Response {
return { status: 200, body: `GET ${req.path} — OK` };
}
}
// ── Chain assembly ──────────────────────────────────────────────
const auth = new AuthenticationMiddleware();
const authz = new AuthorizationMiddleware('admin');
const throttle = new ThrottleMiddleware(3);
const route = new RouteHandler();
auth.setNext(authz).setNext(throttle).setNext(route);
// ── Usage ────────────────────────────────────────────────────
const reqs: Request[] = [
{ path: '/admin', ip: '10.0.0.1' }, // no token
{ path: '/admin', token: 'abc', role: 'user', ip: '10.0.0.2' }, // wrong role
{ path: '/admin', token: 'xyz', role: 'admin', ip: '10.0.0.3' }, // ok
{ path: '/admin', token: 'xyz', role: 'admin', ip: '10.0.0.3' }, // ok
{ path: '/admin', token: 'xyz', role: 'admin', ip: '10.0.0.3' }, // ok
{ path: '/admin', token: 'xyz', role: 'admin', ip: '10.0.0.3' }, // throttle
];
for (const req of reqs) {
const res = auth.process(req);
console.log(`${res.status} — ${res.body}\n`);
}
// 401 — Not authenticated — missing token
// 403 — Access denied — requires role "admin"
// [Auth] → [Authz] → [Throttle] → 200 — GET /admin — OK
// ... (repeated)
// 429 — Limit exceeded for IP 10.0.0.3
<?php
// ── Simplified types ───────────────────────────────────────────
class Request
{
public function __construct(
public readonly string $path,
public readonly string $ip,
public readonly ?string $token = null,
public readonly ?string $role = null // 'admin' | 'user'
) {}
}
class Response
{
public function __construct(
public readonly int $status,
public readonly string $body
) {}
}
// ── Middleware interface ──────────────────────────────────────
interface Middleware
{
public function setNext(Middleware $m): Middleware;
public function process(Request $req): Response;
}
// ── Abstract base class ───────────────────────────────────────
abstract class MiddlewareBase implements Middleware
{
private ?Middleware $next = null;
public function setNext(Middleware $m): Middleware
{
$this->next = $m;
return $m;
}
protected function forward(Request $req): Response
{
return $this->next
? $this->next->process($req)
: new Response(204, 'OK (no final handler)');
}
abstract public function process(Request $req): Response;
}
// ── Middleware 1: authentication ──────────────────────────────
class AuthenticationMiddleware extends MiddlewareBase
{
public function process(Request $req): Response
{
if ($req->token === null) {
return new Response(401, 'Not authenticated — missing token');
}
echo "[Auth] valid token\n";
return $this->forward($req);
}
}
// ── Middleware 2: authorization ───────────────────────────────
class AuthorizationMiddleware extends MiddlewareBase
{
public function __construct(private readonly string $requiredRole) {}
public function process(Request $req): Response
{
if ($req->role !== $this->requiredRole && $req->role !== 'admin') {
return new Response(
403,
"Access denied — requires role \"{$this->requiredRole}\""
);
}
echo "[Authz] role \"{$req->role}\" authorized\n";
return $this->forward($req);
}
}
// ── Middleware 3: throttle by IP ──────────────────────────────
class ThrottleMiddleware extends MiddlewareBase
{
/** @var array<string, int> */
private array $counters = [];
public function __construct(private readonly int $limit) {}
public function process(Request $req): Response
{
$this->counters[$req->ip] = ($this->counters[$req->ip] ?? 0) + 1;
$count = $this->counters[$req->ip];
if ($count > $this->limit) {
return new Response(429, "Limit exceeded for IP {$req->ip}");
}
echo "[Throttle] {$req->ip}: {$count}/{$this->limit}\n";
return $this->forward($req);
}
}
// ── Final handler: route ──────────────────────────────────────
class RouteHandler extends MiddlewareBase
{
public function process(Request $req): Response
{
return new Response(200, "GET {$req->path} — OK");
}
}
// ── Chain assembly ──────────────────────────────────────────────
$auth = new AuthenticationMiddleware();
$authz = new AuthorizationMiddleware('admin');
$throttle = new ThrottleMiddleware(3);
$route = new RouteHandler();
$auth->setNext($authz)->setNext($throttle)->setNext($route);
// ── Usage ────────────────────────────────────────────────────
$reqs = [
new Request('/admin', '10.0.0.1'),
new Request('/admin', '10.0.0.2', 'abc', 'user'),
new Request('/admin', '10.0.0.3', 'xyz', 'admin'),
new Request('/admin', '10.0.0.3', 'xyz', 'admin'),
new Request('/admin', '10.0.0.3', 'xyz', 'admin'),
new Request('/admin', '10.0.0.3', 'xyz', 'admin'), // throttle
];
foreach ($reqs as $req) {
$res = $auth->process($req);
echo "{$res->status} — {$res->body}\n\n";
}
When to use
- When more than one object can process a request and the processor isn't known ahead of time: the sender shouldn't embed routing logic. The chain dynamically decides who acts, based on the request's state.
- When you want to send a request to one of several objects without specifying the receiver explicitly: middleware pipelines, event filters, staged validation flows.
- When the set of handlers should be configurable at runtime: the chain is assembled by the client; handlers can be added, removed, or reordered without modifying the sender or existing handlers.
- To implement validation or pre-processing pipelines where each step is autonomous: each handler only knows its own rule and the forwarding contract — it doesn't know the others.
When to avoid
- When guaranteed processing is mandatory: CoR doesn't guarantee the request will be processed — it can fall through if no handler accepts it. If processing is mandatory, use a fallback final handler or choose another pattern.
- When the chain is very long or nested: long chains are hard to debug — tracing which handler a request went through requires explicit logging at each link. The stack trace can be deep and the root cause obscure.
- When there's exactly one possible handler and it's known statically: in that case, a direct call or Strategy is simpler and more readable.
Pros and cons
Pros
- Decouples the sender from the receivers — the sender knows only the first handler of the chain.
- Respects the Single Responsibility Principle: each handler has one handling rule.
- Respects the Open/Closed Principle: new handlers can be inserted without modifying existing ones or the sender.
- The chain is configurable at runtime — handlers can be reordered, added, or removed dynamically.
Cons
- Doesn't guarantee processing — the request can fall through if no handler acts (requires an intentional fallback handler).
- Makes debugging harder: the execution flow isn't obvious just by reading the code that assembles the chain.
- Long chains can have a performance impact — each handler adds a method call.
- Responsibility can become diffuse: when many handlers have overlapping rules, it's hard to know which one will process a given request.
Common pitfalls
1. A request that falls through (no final handler)
The most common mistake: assembling the chain without a fallback
handler at the end. If no handler in the chain accepts the
request, it silently returns null — and the calling
code may not be prepared for that, causing a
NullPointerException or undefined behavior.
Rule of thumb: always add an explicit final
handler that deals with the case "no earlier handler wanted to
process this". It can be a warning logger, an exception thrower, or
a default error response. Never rely on implicit null
as an indicator of "unhandled".
2. A chain that never ends (loop)
If a handler calls super.handle() without checking the
condition — or if a handler is accidentally added to the end of its
own chain — CoR can enter an infinite loop or deep recursion. Make
sure every ConcreteHandler has a clear condition to stop the chain
and that no handler references itself as the next.
3. Diffuse responsibility — handlers that "listen" instead of "filter"
A frequent antipattern: handlers that always call
super.handle() regardless of the result — they process
AND forward. This turns CoR into a disguised Observer, where every
handler always runs. CoR's Single Responsibility Principle is: you
either handle and stop, or don't handle and pass. If the domain
requires everyone to handle it, use Observer or a listener list.
4. Chain of Responsibility vs Strategy — the intent distinction
The confusion arises because both encapsulate behavior in objects with a common interface. The difference is structural and about intent:
- Strategy: exactly one algorithm is chosen by the context and applied. There's no chain, no forwarding. The context decides which Strategy to use — and only that one runs.
- Chain of Responsibility: zero or more handlers process the request sequentially, each able to stop or continue. None knows the others. The set that will process it is determined at runtime by the request's state.
5. Distinction from Decorator
Structurally similar: both chain objects with the same interface. The difference is that Decorator always delegates to the next (adding behavior before or after, never interrupting) and its intent is adding responsibilities to the decorated object. CoR can interrupt the chain without delegating — when it processes the request, the following handlers aren't called. If the object always passes it along AND adds behavior, it's Decorator. If it can stop the chain without passing it, it's CoR.
Related patterns
Chain of Responsibility is frequently combined with or compared to patterns that also deal with action dispatch and behavior composition:
Command and CoR frequently appear together: Command encapsulates the request as an object, and CoR decides which handler will process it — the Command carries the action's data; CoR routes who acts on it. Decorator is the pattern with the greatest structural similarity: both chain objects with the same interface. The key difference is that Decorator always delegates to the next and adds behavior without interrupting the chain, while CoR can stop the chain when it processes the request — the two have opposite intents regarding control flow. Visitor can be used to process the nodes of a CoR chain without modifying the handler classes, separating the traversal logic from the handling logic.