3 min read

Chain of Responsibility Pattern

The Chain of Responsibility Pattern is a behavioral design pattern that 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.

1. The Core Concept

This pattern decouples the sender of a request from its receivers by giving more than one object a chance to handle the request.

  • Handler: An interface or abstract class declaring the method for handling requests and optionally setting the next handler.
  • Concrete Handlers: Implement the handling logic. If they can handle the request, they do; otherwise, they forward it to the next handler.
  • Client: Initiates the request to the first handler in the chain.

2. Analogy: Tech Support

  1. Level 1 Support: Receives a call. If it's a simple password reset, they handle it. If not, they pass it to Level 2.
  2. Level 2 Support: Checks if it's a known software bug. If yes, they fix it. If not (e.g., server crash), they pass it to Level 3 (DevOps).
  3. Level 3 Support: Fixes the server.

The customer (Client) just calls one number and doesn't need to know the internal hierarchy.

3. Example (TypeScript)

Let's implement a simple logging chain where different loggers handle different severity levels.

// Handler Interface
interface Logger {
    setNext(next: Logger): Logger;
    log(message: string, level: string): void;
}

// Abstract Base Handler
abstract class AbstractLogger implements Logger {
    private nextLogger: Logger | null = null;

    setNext(next: Logger): Logger {
        this.nextLogger = next;
        return next; // Return next handler for convenient chaining
    }

    log(message: string, level: string): void {
        if (this.nextLogger) {
            this.nextLogger.log(message, level);
        }
    }
}

// Concrete Handler 1: Info
class InfoLogger extends AbstractLogger {
    log(message: string, level: string): void {
        if (level === "INFO") {
            console.log(`[INFO]: ${message}`);
        } else {
            super.log(message, level);
        }
    }
}

// Concrete Handler 2: Error
class ErrorLogger extends AbstractLogger {
    log(message: string, level: string): void {
        if (level === "ERROR") {
            console.error(`[ERROR]: ${message}`);
        } else {
            super.log(message, level);
        }
    }
}

// Usage
const loggerChain = new InfoLogger();
loggerChain
    .setNext(new ErrorLogger());

loggerChain.log("System started", "INFO");   // Handled by InfoLogger
loggerChain.log("Null pointer exception", "ERROR"); // Passed to ErrorLogger

4. Pros and Cons

Pros Cons
Decoupling: The client doesn't know which specific object handles the request. Uncertainty: There is no guarantee that the request will be handled (it might fall off the end of the chain).
SRP: You can decouple classes that invoke operations from classes that perform operations. Performance: Long chains can introduce latency.
Flexibility: You can change the chain structure dynamically at runtime. Debugging: It can be hard to trace the flow of a request through a complex chain.

5. Middleware (Express/Redux)

The most common modern implementation of the Chain of Responsibility pattern is Middleware in web frameworks (like Express.js) and state management libraries (like Redux).

Express.js Middleware

In Express, every request passes through a chain of middleware functions. Each function can modify the request/response objects, end the cycle, or call next() to pass control to the next middleware.

const app = express();

// Middleware 1: Logger
app.use((req, res, next) => {
    console.log('Request received');
    next(); // Pass to next handler
});

// Middleware 2: Auth Check
app.use((req, res, next) => {
    if (req.headers.authorization) {
        next(); // Pass to next handler
    } else {
        res.status(401).send('Unauthorized'); // Break the chain
    }
});

// Final Handler (Route)
app.get('/', (req, res) => {
    res.send('Hello World');
});

Redux Middleware

Redux middleware sits between dispatching an action and the moment it reaches the reducer. It forms a chain where actions can be logged, modified, or delayed (e.g., Redux Thunk) before reaching the state update logic.

programming/design-patterns programming/object-oriented-programming