Low-level design3 min
Chain of Responsibility
Type: Behavioral Pattern
Relevance: Medium. Often used in HTTP request middleware (like Express.js) or UI event bubbling.
The Chain of Responsibility pattern lets you pass requests along a chain of handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.
Real-Life Analogy
Think of calling Customer Support. You have a technical issue. You first speak to the Tier 1 Bot. The Bot can't fix it, so it passes you to a Tier 2 Human Agent. The agent doesn't have the authority to grant you a refund, so they pass you to the Tier 3 Manager, who finally resolves your issue.
The request travels down the chain until a specific handler is capable of processing it.
The Problem
Imagine an application that receives a request. You need to authenticate the user, then authorize their roles, then validate the data payload, and finally save it to the DB. If you put all those if statements into one massive function, the code becomes unmaintainable.
How to Implement Chain of Responsibility
Define a Handler interface with a setNext() method and a handle() method. Create concrete handlers. Each handler either processes the request and breaks the chain, or passes it to this.nextHandler.handle().
Example in Code
// 1. The Handler Interface
interface Handler {
setNext(handler: Handler): Handler;
handle(request: string): string | null;
}
// 2. The Base Handler
abstract class AbstractHandler implements Handler {
private nextHandler: Handler | null = null;
public setNext(handler: Handler): Handler {
this.nextHandler = handler;
// Returning a handler from here lets us link them in a convenient way
return handler;
}
public handle(request: string): string | null {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}
// 3. Concrete Handlers
class AuthHandler extends AbstractHandler {
public handle(request: string): string | null {
if (request === "NoToken") {
return "AuthHandler: Access Denied. No Token.";
}
console.log("AuthHandler: Token valid, passing to next...");
return super.handle(request); // Pass to next in chain
}
}
class ValidationHandler extends AbstractHandler {
public handle(request: string): string | null {
if (request === "BadPayload") {
return "ValidationHandler: Payload is invalid.";
}
console.log("ValidationHandler: Payload valid, passing to next...");
return super.handle(request);
}
}
class DatabaseHandler extends AbstractHandler {
public handle(request: string): string | null {
return `DatabaseHandler: Successfully saved [${request}] to database.`;
}
}
// Client Code
const auth = new AuthHandler();
const validation = new ValidationHandler();
const database = new DatabaseHandler();
// Linking the chain!
auth.setNext(validation).setNext(database);
// Let's test the chain
console.log(auth.handle("ValidData"));
// Auth -> Validation -> DB (Saved)
console.log(auth.handle("BadPayload"));
// Auth -> Validation (Stops here, returns invalid payload)
Class Diagram
Why is it Important?
- Decoupling: The client doesn't need to know which object in the chain will actually process the request.
- Flexibility: You can dynamically insert, remove, or reorder handlers in the chain at runtime without changing client code.