Chain of Responsibility
Pass requests along a chain of handlers. Each handler decides to process the request or pass it to the next handler. Decouples senders from receivers and lets you dynamically configure the chain.
The Problem
You’re building a UI event system. A button click should: (1) check if the button has a click handler, (2) if not, check its parent panel, (3) if not, check the window, (4) finally, if nothing handles it, discard it. But the button doesn’t know who its parent is. And hardcoding the chain in one place makes it rigid — you can’t reorder or add handlers without touching that code.
More generally: you have multiple objects that might handle a request, and you don’t know which one will at the time the request is made.
The Solution
Chain the handlers together. Each handler holds a reference to the next one. When a request arrives, the handler either processes it and stops, or forwards it down the chain. The sender only talks to the first handler — it doesn’t know about the chain at all.
Real-World Analogy
A support ticket system. You submit a ticket; it first goes to Tier 1 support. If they can solve it, done. If not, they escalate to Tier 2. If Tier 2 can’t handle it, it goes to engineering. You (the sender) just submit the ticket — you don’t know which tier will ultimately solve it.
TypeScript Example
// Handler interface
interface AuthHandler {
setNext(handler: AuthHandler): AuthHandler;
handle(request: AuthRequest): AuthResponse;
}
interface AuthRequest {
token: string;
ip: string;
userAgent: string;
resource: string;
}
interface AuthResponse {
allowed: boolean;
reason: string;
}
// Abstract base handler — wires the chain
abstract class BaseAuthHandler implements AuthHandler {
private nextHandler: AuthHandler | null = null;
setNext(handler: AuthHandler): AuthHandler {
this.nextHandler = handler;
return handler; // allows chaining: h1.setNext(h2).setNext(h3)
}
handle(request: AuthRequest): AuthResponse {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
// End of chain — default allow (or deny, depending on policy)
return { allowed: true, reason: 'passed all checks' };
}
}
// Concrete handlers
class IPBlocklistHandler extends BaseAuthHandler {
private blocked = new Set(['192.168.1.100', '10.0.0.5']);
handle(request: AuthRequest): AuthResponse {
if (this.blocked.has(request.ip)) {
return { allowed: false, reason: `IP ${request.ip} is blocked` };
}
console.log(`[IPCheck] ${request.ip} OK`);
return super.handle(request); // pass to next
}
}
class TokenValidationHandler extends BaseAuthHandler {
handle(request: AuthRequest): AuthResponse {
if (!request.token || request.token.length < 10) {
return { allowed: false, reason: 'Invalid or missing token' };
}
console.log(`[TokenCheck] Token OK`);
return super.handle(request);
}
}
class RateLimitHandler extends BaseAuthHandler {
private counts = new Map<string, number>();
handle(request: AuthRequest): AuthResponse {
const count = (this.counts.get(request.ip) ?? 0) + 1;
this.counts.set(request.ip, count);
if (count > 5) {
return { allowed: false, reason: `Rate limit exceeded for ${request.ip}` };
}
console.log(`[RateLimit] Request ${count}/5 from ${request.ip}`);
return super.handle(request);
}
}
class ResourcePermissionHandler extends BaseAuthHandler {
private adminResources = new Set(['/admin', '/settings']);
handle(request: AuthRequest): AuthResponse {
if (this.adminResources.has(request.resource) && !request.token.startsWith('admin-')) {
return { allowed: false, reason: `Resource ${request.resource} requires admin token` };
}
console.log(`[Permission] Resource OK`);
return super.handle(request);
}
}
// Build the chain
const ipCheck = new IPBlocklistHandler();
const tokenCheck = new TokenValidationHandler();
const rateLimit = new RateLimitHandler();
const permissions = new ResourcePermissionHandler();
ipCheck.setNext(tokenCheck).setNext(rateLimit).setNext(permissions);
// Test cases
const requests: AuthRequest[] = [
{ token: 'user-abc123xyz', ip: '10.0.0.5', userAgent: 'Chrome', resource: '/dashboard' },
{ token: '', ip: '1.2.3.4', userAgent: 'Chrome', resource: '/dashboard' },
{ token: 'user-abc123xyz', ip: '1.2.3.4', userAgent: 'Chrome', resource: '/admin' },
{ token: 'admin-abc123xyz',ip: '1.2.3.4', userAgent: 'Chrome', resource: '/admin' },
];
for (const req of requests) {
console.log(`\nRequest: ${req.ip} → ${req.resource}`);
const result = ipCheck.handle(req);
console.log(`Result: ${result.allowed ? '✓ ALLOWED' : '✗ DENIED'} — ${result.reason}`);
}
When to Use
- Multiple objects may handle a request, and you don’t know which one at the time the request is made
- You want to decouple the sender from the receiver
- You need to issue a request to one of several objects without specifying the receiver explicitly
- The set of handlers should be specifiable dynamically
- Examples: middleware stacks (Express, Koa), event bubbling in the DOM, auth pipelines, logging filters
Pros
- Single Responsibility — each handler has exactly one check/transform
- Open/Closed — add new handlers without changing the sender or other handlers
- Control the order by changing how the chain is assembled
- A handler can stop processing early or pass through — flexible
Cons
- No guarantee a request will be handled — can silently fall off the end of the chain
- Debugging a chain can be tricky — hard to see which handler processed (or didn’t process) a request at a glance
Related Patterns
- Command encapsulates a request as an object; CoR passes requests along a chain
- Decorator wraps objects to add behaviour; CoR passes requests through handlers
- Composite tree structures can use CoR to bubble events up the tree