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

typescript
// 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.