Loading learning content...
In the previous page, we uncovered a pervasive design problem: how do we route requests to multiple potential handlers without tightly coupling the sender to every receiver? We saw how naive solutions lead to fragile code, testing nightmares, and mounting technical debt.
The Chain of Responsibility pattern provides an elegant solution rooted in a simple insight: instead of the sender knowing all handlers, let each handler know only its successor. Thread handlers together like links in a chain, and let the request flow through until handled. The sender doesn't know who will handle the request—and doesn't need to.
This deceptively simple restructuring transforms rigid, coupled systems into flexible, extensible pipelines. Let's explore how.
By the end of this page, you will understand the core structure of the Chain of Responsibility pattern, including the Handler interface, concrete handlers, and the client's role. You'll implement a complete example, understand how the pattern achieves decoupling, and learn the two primary chain execution strategies: stop-at-first-handler and propagate-through-all.
The Chain of Responsibility pattern belongs to the behavioral pattern category. It addresses how objects communicate and distribute responsibilities—specifically, how to pass requests along a chain of handlers until one processes it.
The core concept:
Instead of a central router that knows all handlers, each handler holds a reference to the next handler in the chain. When a handler receives a request, it either processes the request or forwards it to its successor. The sender interacts only with the first handler in the chain.
This structure inverts the control: handlers decide whether to process or pass, rather than a router deciding which handler to use.
"Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it."
— Design Patterns: Elements of Reusable Object-Oriented Software (1994)
Key participants:
| Participant | Responsibility | Key Characteristics |
|---|---|---|
| Handler (Interface) | Defines the interface for handling requests and optionally implements the successor link | Abstract; may provide default chaining behavior |
| ConcreteHandler | Handles requests it is responsible for; forwards unhandled requests to successor | Contains handling logic and decision whether to process |
| Client | Initiates the request to a handler in the chain | Knows only the chain head; unaware of internal structure |
| Request | The data or command being passed through the chain | May be modified as it passes through handlers |
123456789101112131415
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐│ Client │──────▶│ Handler A │──────▶│ Handler B │──────▶│ Handler C ││ │ │ │ │ │ │ ││ Sends │ │ Can handle? │ │ Can handle? │ │ Can handle? ││ Request │ │ → Process │ │ → Process │ │ → Process ││ │ │ → Or pass │ │ → Or pass │ │ → End chain │└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ ▼ ▼ ▼ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ └─────────────▶│ Request │──────────▶│ Request │───────────▶│ Request │ └─────────┘ └─────────┘ └─────────┘ Request flows through chain until handled or chain ends.Client knows only Handler A. Handlers B and C are invisible to Client.The foundation of the Chain of Responsibility pattern is the Handler interface. This interface defines the contract that all handlers must fulfill, creating the polymorphism that makes the chain possible.
Let's design a robust handler interface step by step:
12345678910111213141516171819202122232425262728
/** * The Handler interface declares the contract for handling requests. * * Design decisions: * 1. Generic type T allows handlers for different request types * 2. setNext returns Handler, enabling fluent chaining * 3. handle returns optional result, supporting both void and return-value chains */interface Handler<T, R = void> { /** * Sets the next handler in the chain. * Returns this handler to enable fluent chaining: * handlerA.setNext(handlerB).setNext(handlerC) */ setNext(handler: Handler<T, R>): Handler<T, R>; /** * Processes the request. * May handle the request, pass it to the next handler, or both. * Returns null/undefined if no result, or R if a result is produced. */ handle(request: T): R | null;} // Example: For our purchase order systeminterface PurchaseOrderHandler extends Handler<PurchaseOrder, ApprovalResult> { // Inherits setNext and handle, typed for PurchaseOrder and ApprovalResult}Why this design?
The interface is intentionally minimal. It defines what handlers do, not how they do it. This abstraction is what enables the decoupling we need:
Notice that the interface doesn't dictate whether a handler must forward to its successor or when. That remains the handler's responsibility, providing maximum flexibility.
The setNext method returns Handler<T, R>, enabling fluent chain construction: a.setNext(b).setNext(c). This pattern, borrowed from builder design, makes chain assembly readable and concise. The return value is the this handler, not the next handler, allowing left-to-right chain construction.
While handlers can implement the interface directly, a common practice is to provide an abstract base class that handles the chaining mechanics. This follows the Template Method pattern—the base class provides the skeleton, and subclasses fill in the specific handling logic.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
/** * Abstract base class that provides default chaining behavior. * Concrete handlers extend this class and implement the handling logic. */abstract class AbstractHandler<T, R = void> implements Handler<T, R> { private nextHandler: Handler<T, R> | null = null; /** * Sets the next handler and returns this handler for chaining. * This implementation is typically sufficient for all concrete handlers. */ setNext(handler: Handler<T, R>): Handler<T, R> { this.nextHandler = handler; // Return this handler, not the next one, for fluent chaining return this; } /** * The main handling method. * * Default behavior: pass to next handler if it exists. * Subclasses override to add their handling logic before/after calling super. * * This is a Template Method - defines the algorithm skeleton. */ handle(request: T): R | null { if (this.nextHandler) { return this.nextHandler.handle(request); } return null; // End of chain } /** * Protected method to explicitly forward to next handler. * Useful when subclasses need to forward after partial processing. */ protected forward(request: T): R | null { if (this.nextHandler) { return this.nextHandler.handle(request); } return null; } /** * Utility to check if there is a next handler. */ protected hasNext(): boolean { return this.nextHandler !== null; }}The base class responsibilities:
nextHandler field holds the next link in the chainsetNext — Provides the standard chaining behavior that concrete handlers inherithandle — The default just forwards to the next handlerforward() and hasNext() help concrete handlers implement their logicHow concrete handlers extend this:
Concrete handlers override handle() to add their specific logic. They decide whether to:
forward())Using an abstract class is not the only approach. Alternatively, you could use composition—each handler holds a 'next' handler reference directly without inheritance. The class approach is shown here because it's the classical GoF implementation, but composition-based approaches are equally valid and sometimes preferable in languages with interface-based design.
Now let's implement concrete handlers for our purchase order approval system. Each handler encapsulates its approval logic and decides whether to process or forward.
123456789101112131415161718
// The request type flowing through the chainclass PurchaseOrder { constructor( public readonly id: string, public readonly amount: number, public readonly requesterId: string, public readonly description: string, public readonly priority: 'LOW' | 'NORMAL' | 'HIGH' | 'URGENT' = 'NORMAL' ) {}} // The result type returned by handlersinterface ApprovalResult { approved: boolean; approver: string; comments?: string; timestamp: Date;}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
/** * Supervisor Handler - approves orders up to $1,000 */class SupervisorHandler extends AbstractHandler<PurchaseOrder, ApprovalResult> { private readonly approvalLimit = 1000; private readonly approverName = 'Supervisor'; handle(order: PurchaseOrder): ApprovalResult | null { // Check if this handler can process the request if (order.amount <= this.approvalLimit) { // Handle the request - this handler is responsible console.log(`Supervisor approving order ${order.id} for $${order.amount}`); return { approved: true, approver: this.approverName, comments: `Approved within supervisor limit ($${this.approvalLimit})`, timestamp: new Date(), }; } // Cannot handle - forward to next handler console.log(`Order ${order.id} ($${order.amount}) exceeds supervisor limit, forwarding...`); return this.forward(order); }} /** * Manager Handler - approves orders up to $10,000 */class ManagerHandler extends AbstractHandler<PurchaseOrder, ApprovalResult> { private readonly approvalLimit = 10000; private readonly approverName = 'Manager'; handle(order: PurchaseOrder): ApprovalResult | null { if (order.amount <= this.approvalLimit) { console.log(`Manager approving order ${order.id} for $${order.amount}`); return { approved: true, approver: this.approverName, comments: `Approved within manager limit ($${this.approvalLimit})`, timestamp: new Date(), }; } console.log(`Order ${order.id} ($${order.amount}) exceeds manager limit, forwarding...`); return this.forward(order); }} /** * Director Handler - approves orders up to $100,000 */class DirectorHandler extends AbstractHandler<PurchaseOrder, ApprovalResult> { private readonly approvalLimit = 100000; private readonly approverName = 'Director'; handle(order: PurchaseOrder): ApprovalResult | null { if (order.amount <= this.approvalLimit) { console.log(`Director approving order ${order.id} for $${order.amount}`); return { approved: true, approver: this.approverName, comments: `Approved within director limit ($${this.approvalLimit})`, timestamp: new Date(), }; } console.log(`Order ${order.id} ($${order.amount}) exceeds director limit, forwarding...`); return this.forward(order); }} /** * CEO Handler - can approve any amount (final handler in chain) */class CEOHandler extends AbstractHandler<PurchaseOrder, ApprovalResult> { private readonly approverName = 'CEO'; handle(order: PurchaseOrder): ApprovalResult | null { // CEO handles everything that reaches this point console.log(`CEO approving order ${order.id} for $${order.amount}`); return { approved: true, approver: this.approverName, comments: 'Approved by CEO - no limit', timestamp: new Date(), }; // Note: CEO doesn't forward - they're the final authority }}Notice the key characteristics of these handlers:
this.forward() when they can't handleEach handler is completely independent—it can be unit tested in isolation, reordered in the chain, or removed entirely without affecting other handlers. This is the decoupling the pattern provides: each component is self-contained and unaware of the broader chain structure.
With our handlers defined, we need to assemble them into a chain and use it. The client is responsible for chain construction but interacts only with the chain head for processing.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
/** * Chain assembly - typically done in application configuration */function createApprovalChain(): Handler<PurchaseOrder, ApprovalResult> { const supervisor = new SupervisorHandler(); const manager = new ManagerHandler(); const director = new DirectorHandler(); const ceo = new CEOHandler(); // Fluent chain construction supervisor .setNext(manager) .setNext(director) .setNext(ceo); // Return only the chain head - the rest is hidden return supervisor;} /** * Client code using the chain */class PurchaseOrderProcessor { private readonly approvalChain: Handler<PurchaseOrder, ApprovalResult>; constructor(approvalChain: Handler<PurchaseOrder, ApprovalResult>) { // The processor knows only about the Handler interface, not concrete handlers this.approvalChain = approvalChain; } processOrder(order: PurchaseOrder): ApprovalResult | null { console.log(`Processing order ${order.id}: $${order.amount}`); // Submit to the chain - the processor doesn't know which handler will respond const result = this.approvalChain.handle(order); if (result) { console.log(`Result: ${result.approved ? 'APPROVED' : 'DENIED'} by ${result.approver}`); } else { console.log('Result: No handler could process this request'); } return result; }} // Usage exampleconst approvalChain = createApprovalChain();const processor = new PurchaseOrderProcessor(approvalChain); // Test with different amountsprocessor.processOrder(new PurchaseOrder('PO-001', 500, 'alice', 'Office supplies'));// Output: Supervisor approves processor.processOrder(new PurchaseOrder('PO-002', 5000, 'bob', 'Server equipment'));// Output: Forwards past Supervisor → Manager approves processor.processOrder(new PurchaseOrder('PO-003', 75000, 'carol', 'Department renovation'));// Output: Forwards past Supervisor → Manager → Director approves processor.processOrder(new PurchaseOrder('PO-004', 500000, 'dave', 'Acquisition target'));// Output: Forwards through entire chain → CEO approvesThe Chain of Responsibility pattern supports multiple execution strategies depending on requirements. Understanding these strategies is essential for applying the pattern correctly.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
/** * Middleware chain where every handler processes the request. * Common in HTTP middleware, validation pipelines, logging chains. */interface Request { path: string; method: string; headers: Record<string, string>; body?: unknown; context: Record<string, unknown>; // Handlers add context as they process} abstract class Middleware extends AbstractHandler<Request, Request | null> {} /** * Logging middleware - always logs, always forwards */class LoggingMiddleware extends Middleware { handle(request: Request): Request | null { // Process: add log entry console.log(`[${new Date().toISOString()}] ${request.method} ${request.path}`); // ALWAYS forward to next - this is process-and-pass return this.forward(request); }} /** * Authentication middleware - adds user context, always forwards */class AuthMiddleware extends Middleware { handle(request: Request): Request | null { const authHeader = request.headers['Authorization']; if (authHeader) { // Process: validate token and add user to context const user = this.validateToken(authHeader); request.context['user'] = user; } // ALWAYS forward - let downstream handlers check for user return this.forward(request); } private validateToken(header: string): { id: string; role: string } | null { // Token validation logic... return { id: 'user123', role: 'admin' }; }} /** * CORS middleware - adds headers, always forwards */class CORSMiddleware extends Middleware { handle(request: Request): Request | null { // Process: add CORS context request.context['cors'] = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE', }; // ALWAYS forward return this.forward(request); }} // Chain where all handlers processconst middlewareChain = new LoggingMiddleware() .setNext(new AuthMiddleware()) .setNext(new CORSMiddleware()); // Request flows through ALL handlers, each adding to the contextSome chains use hybrid strategies. For example, a validation chain might process-and-pass normally, but stop propagation if validation fails. HTTP middleware might pass through all handlers unless one explicitly terminates (like a rate limiter rejecting a request). The pattern is flexible enough to accommodate these variations.
Let's systematically analyze the benefits the Chain of Responsibility pattern provides, comparing against the naive approach we examined in the previous page:
| Aspect | Naive Approach | Chain of Responsibility |
|---|---|---|
| Coupling | Client coupled to all handlers | Client coupled only to Handler interface |
| Adding Handlers | Modify client code | Insert handler in chain; no client changes |
| Removing Handlers | Modify client code | Remove from chain; no client changes |
| Reordering | Modify client code | Reconstruct chain; no client changes |
| Unit Testing | Requires all handlers instantiated | Each handler tested in isolation |
| Single Responsibility | Client handles routing + business logic | Each handler has one responsibility |
| Open/Closed | Client modified for every handler change | Add new handlers without modifying existing code |
The pattern transforms testing from integration-heavy to unit-test-friendly. Each handler can be tested with a mock successor, verifying its logic in isolation. The chain assembly can be tested separately by verifying correct wiring. And client code can be tested with a mock handler that simulates any chain behavior.
No pattern is without trade-offs. Understanding when the Chain of Responsibility pattern is appropriate—and when it's not—is essential for effective application.
Mitigation strategies:
12345678910111213141516171819202122
/** * Fallback handler that terminates the chain with explicit handling */class FallbackHandler extends AbstractHandler<PurchaseOrder, ApprovalResult> { handle(order: PurchaseOrder): ApprovalResult | null { // This handler ALWAYS handles - it's the safety net console.error(`No handler could approve order ${order.id}`); return { approved: false, approver: 'System', comments: 'Request exceeded all approval thresholds', timestamp: new Date(), }; }} // Chain with fallbacksupervisor .setNext(manager) .setNext(director) .setNext(ceo) .setNext(new FallbackHandler()); // Safety net at the end12345678910111213141516171819202122232425262728293031
/** * Decorated handler that adds logging for debugging */class LoggingHandlerDecorator<T, R> implements Handler<T, R> { constructor( private readonly handler: Handler<T, R>, private readonly handlerName: string ) {} setNext(handler: Handler<T, R>): Handler<T, R> { this.handler.setNext(handler); return this; } handle(request: T): R | null { console.log(`[DEBUG] ${this.handlerName} received request`); const result = this.handler.handle(request); if (result) { console.log(`[DEBUG] ${this.handlerName} handled request`); } else { console.log(`[DEBUG] ${this.handlerName} forwarded request`); } return result; }} // Usage: wrap handlers for debuggingconst debugSupervisor = new LoggingHandlerDecorator( new SupervisorHandler(), 'SupervisorHandler');We've explored how the Chain of Responsibility pattern solves the request routing problem through handler chaining. Let's consolidate our understanding:
What's next:
Now that we understand the pattern structure, we'll explore how to construct chains effectively. The next page covers chain construction patterns—how to assemble handlers into chains, configure them dynamically, and manage chain lifecycle in production systems.
You now understand the core solution provided by the Chain of Responsibility pattern. You've seen how the Handler interface, abstract base class, and concrete handlers work together to create decoupled, extensible request processing. With this foundation, you're ready to explore advanced chain construction techniques.