LLDBehavioral Design PatternsChain of Responsibility

Chain of Responsibility

Behavioral Design Patterns

Imagine an incoming API request that must pass several sequential checks: authentication, authorization, throttling, and logging. A naive approach is to cram all of this logic into a single, monolithic method:

// Anti-Pattern: A single method handling multiple, sequential concerns.
public class MonolithicProcessor {
    public void process(ApiRequest request) {
        // 1. Authentication check
        if (!request.isAuthenticated()) {
            System.out.println("ERROR: Authentication failed!");
            return; // Stop processing
        }

        // 2. Authorization check
        if (!"Admin".equals(request.getUserRole())) {
            System.out.println("ERROR: Authorization failed! Admin role required.");
            return; // Stop processing
        }

        // 3. Throttling check
        // (Imagine complex throttling logic here...)
        if (isThrottled(request.getUserId())) {
             System.out.println("ERROR: Throttling limit exceeded!");
             return; // Stop processing
        }

        System.out.println("Request processed successfully!");
    }

    private boolean isThrottled(String userId) { /* ... */ return false; }
}

This design is rigid. Every time a new check is required (e.g., caching, logging), you must modify this central method, which violates the Open/Closed Principle. The processor is tightly coupled to every concern, making it hard to maintain, reorder, and test. How can we decouple the request from the series of checks it must pass through?

Passing the Request Along a Chain

The Chain of Responsibility is a behavioral design pattern that lets you pass a request along a chain of potential handlers. Upon receiving a request, each handler decides either to process the request or to pass it to the next handler in the chain.

The core idea is to decouple the sender of a request from its receiver. The sender doesn't know which object in the chain will ultimately handle the request. This allows you to build a flexible pipeline where you can add, remove, or reorder handlers dynamically without affecting the client.

The Participants

  1. Handler: The interface or abstract class for all handlers in the chain. It declares a method for processing requests and, crucially, holds a reference to the next handler in the chain.
  2. Concrete Handler: A specific implementation of the Handler. It checks if it can handle the request. If yes, it processes it and stops the chain. If no, it delegates the request to the next handler it holds a reference to.
  3. Client: The class that creates the chain of handlers and initiates the request by sending it to the first handler in the chain.

Example: An API Request Authorization System

Let's model a system that checks an incoming API request. The chain will be:

  1. Authentication Handler: Checks if the user is logged in.
  2. Authorization Handler: Checks if the logged-in user has the required permissions.
  3. Throttling Handler: Checks if the user has exceeded their request limit.
// The Handler interface defines the contract for all handlers.
interface RequestHandler {
    void handleRequest(ApiRequest request);
    RequestHandler setNext(RequestHandler handler);
}

// An abstract base class to manage the "next" handler reference and chaining.
abstract class BaseHandler implements RequestHandler {
    protected RequestHandler nextHandler;

    // The fluent setter makes building the chain clean and readable.
    public RequestHandler setNext(RequestHandler handler) {
        this.nextHandler = handler;
        return handler;
    }

    // A helper to pass the request to the next handler, avoiding null checks in subclasses.
    protected void passToNext(ApiRequest request) {
        if (nextHandler != null) {
            nextHandler.handleRequest(request);
        }
    }
}

// A concrete handler for authentication
class AuthenticationHandler extends BaseHandler {
    @Override
    public void handleRequest(ApiRequest request) {
        if (!request.isAuthenticated()) {
            System.out.println("ERROR: Authentication failed!");
            return; // Stop the chain
        }
        System.out.println("Authentication OK");

        passToNext(request);
    }
}

// A concrete handler for authorization
class AuthorizationHandler extends BaseHandler {
    @Override
    public void handleRequest(ApiRequest request) {
        if (request.getUserRole() != "Admin") {
            System.out.println("ERROR: Authorization failed! Admin role required.");
            return; // Stop the chain
        }
        System.out.println("Authorization OK");

        passToNext(request);
    }
}

class ThrottlingHandler extends BaseHandler {
    private final int requestsPerMinute;
    private int requestCount;
    private long currentTime;

    public ThrottlingHandler(int requestsPerMinute) {
        this.requestsPerMinute = requestsPerMinute;
        this.currentTime = System.currentTimeMillis();
    }

    @Override
    public void handleRequest(ApiRequest request) {
        // Reset the count if a minute has passed
        if (System.currentTimeMillis() > currentTime + 60_000) {
            this.requestCount = 0;
            this.currentTime = System.currentTimeMillis();
        }
        requestCount++;
        if (requestCount > requestsPerMinute) {
            System.out.println("ERROR: Throttling limit exceeded!");
            return; // Stop the chain
        }
        System.out.println("Throttling OK");

        passToNext(request);
    }
}

// The client builds the chain and initiates the request
class Client {
    public static void main(String[] args) {
        // Build the chain of responsibility
        RequestHandler chain = new AuthenticationHandler();

        chain.setNext(new AuthorizationHandler())
                .setNext(new ThrottlingHandler(2)); // 2 requests per minute limit

        // Send a request to the start of the chain
        ApiRequest successfulRequest = new ApiRequest(true, "Admin");
        System.out.println("Processing successful request...");
        chain.handleRequest(successfulRequest);

        System.out.println("\nProcessing failed request...");
        ApiRequest failedRequest = new ApiRequest(true, "Guest");
        chain.handleRequest(failedRequest);

        // Test throttling
        for (int i = 0; i < 3; i++) {
            System.out.println("\nProcessing request " + (i + 1) + "...");
            chain.handleRequest(successfulRequest);
        }
    }
}

Interview Focus: Analysis and Trade-offs

Benefits:

  • Decoupling: The sender of the request is completely decoupled from the concrete handlers. The sender only needs a reference to the first handler in the chain.
  • Flexibility and OCP: This is a key advantage. You can add new handlers (like a CachingHandler) or reorder the existing handlers at runtime without changing the client or other handlers.
  • Single Responsibility Principle: Each handler has a single, well-defined responsibility, making the classes cleaner and easier to manage than one monolithic processor.

Drawbacks:

  • Request May Not Be Handled: There's no guarantee that a request will be handled. If it reaches the end of the chain without being processed, it simply gets dropped.
  • Debugging Complexity: Tracing the flow of a request can sometimes be more difficult than following a simple if-else block, as it involves runtime delegation across multiple objects.

How Chain of Responsibility Relates to Other Concepts

  • A Dynamic Alternative to if-else: The pattern's primary purpose is to provide a more flexible, object-oriented alternative to rigid conditional statements for processing a request.
  • Middleware Pipelines: This is the most important modern application of the pattern. Web frameworks like Express.js (Node.js), Django (Python), and ASP.NET Core use the Chain of Responsibility pattern to create "middleware pipelines." Each incoming HTTP request passes through a chain of middleware components (for logging, authentication, CORS, etc.) before it reaches the final endpoint handler.
  • Command Pattern: The "request" object that is passed along the chain is often implemented using the Command pattern. Encapsulating the request as an object allows you to easily pass it from one handler to the next.