Chain of Responsibility

MaverickMaverick
7 min read

Introduction

Have you ever needed to process a request through a series of handlers, where each handler decides whether to process the request or pass it along? If so, you’ve already encountered the core idea behind the Chain of Responsibility design pattern!

This pattern is a behavioral design pattern that allows you to pass requests along a chain of handlers. Each handler can either process the request or pass it to the next handler in the chain. This decouples the sender of a request from its receivers, giving you flexibility and scalability in your code.

Real-World Analogy

Imagine a customer support system:

  • Level 1 Support handles basic issues.

  • If they can’t solve it, they escalate to Level 2 Support.

  • If Level 2 can’t solve it, it goes to Level 3 Support.

This escalation process is a perfect example of the Chain of Responsibility!


Structure Diagram

graph TD;
    Client-->Handler1;
    Handler1-->Handler2;
    Handler2-->Handler3;
    Handler3-->NullHandler;
    Handler1["Handler 1"];
    Handler2["Handler 2"];
    Handler3["Handler 3"];
    NullHandler["(End of Chain)"];

Each handler decides:

  • "Can I handle this?" If yes, process it.
  • If not, pass it to the next handler.

Key Participants

  • Handler (Abstract Class/Interface): Declares a method for handling requests and a reference to the next handler.

  • ConcreteHandler: Implements the handler’s logic. If it can’t handle the request, it passes it to the next handler.

  • Client: Initiates the request.


When to Use Chain of Responsibility

  • When more than one object may handle a request, and the handler isn’t known a priori.

  • When you want to decouple the sender and receiver.

  • When you want to add or change handlers dynamically.


Implementation examples

Java Example: Step-by-Step

Step 1: Define the Handler

abstract class Handler {
    protected Handler next;
    public void setNext(Handler next) {
        this.next = next;
    }
    public abstract void handleRequest(String request);
}

Step 2: Create Concrete Handlers

class AuthHandler extends Handler {
    public void handleRequest(String request) {
        if (request.equals("AUTH")) {
            System.out.println("AuthHandler: Handling AUTH request");
        } else if (next != null) {
            next.handleRequest(request);
        }
    }
}

class LoggingHandler extends Handler {
    public void handleRequest(String request) {
        if (request.equals("LOG")) {
            System.out.println("LoggingHandler: Handling LOG request");
        } else if (next != null) {
            next.handleRequest(request);
        }
    }
}

class DataHandler extends Handler {
    public void handleRequest(String request) {
        if (request.equals("DATA")) {
            System.out.println("DataHandler: Handling DATA request");
        } else if (next != null) {
            next.handleRequest(request);
        } else {
            System.out.println("No handler found for request: " + request);
        }
    }
}

Step 3: Set Up the Chain

public class Main {
    public static void main(String[] args) {
        Handler auth = new AuthHandler();
        Handler log = new LoggingHandler();
        Handler data = new DataHandler();

        auth.setNext(log);
        log.setNext(data);

        // Test the chain
        auth.handleRequest("AUTH"); // Handled by AuthHandler
        auth.handleRequest("LOG");  // Handled by LoggingHandler
        auth.handleRequest("DATA"); // Handled by DataHandler
        auth.handleRequest("UNKNOWN"); // No handler found
    }
}

Output:

AuthHandler: Handling AUTH request
LoggingHandler: Handling LOG request
DataHandler: Handling DATA request
No handler found for request: UNKNOWN
Real-World Example: OkHttp Interceptor Chain

One of the most popular real-world usages of the Chain of Responsibility pattern in Java is the okhttp3.Interceptor and its Chain mechanism, used for HTTP request/response processing in OkHttp.

Each Interceptor can inspect, modify, or short-circuit the request/response, and then pass it along the chain.

import okhttp3.*;
import java.io.IOException;

// Logging Interceptor
class LoggingInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        System.out.println("Sending request: " + request.url());
        Response response = chain.proceed(request);
        System.out.println("Received response for: " + response.request().url());
        return response;
    }
}

// Authentication Interceptor
class AuthInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request original = chain.request();
        Request authenticated = original.newBuilder()
            .header("Authorization", "Bearer <token>")
            .build();
        return chain.proceed(authenticated);
    }
}

// Usage
public class OkHttpChainExample {
    public static void main(String[] args) throws Exception {
        OkHttpClient client = new OkHttpClient.Builder()
            .addInterceptor(new LoggingInterceptor())
            .addInterceptor(new AuthInterceptor())
            .build();

        Request request = new Request.Builder()
            .url("https://httpbin.org/get")
            .build();

        try (Response response = client.newCall(request).execute()) {
            System.out.println("Response code: " + response.code());
        }
    }
}

How it works:

  • The request passes through the LoggingInterceptor (logs the request), then the AuthInterceptor (adds auth header), then is sent to the server.

  • The response passes back through the interceptors in reverse order.

This is a textbook example of the Chain of Responsibility pattern in a real-world Java library.


Architectural Insights & Advanced Considerations

Dynamic Chain Construction

The chain can be assembled dynamically at runtime, allowing for highly configurable and pluggable systems. This is especially useful in frameworks or platforms where handlers may be added or removed based on configuration or user input.

Chain Termination and Short-Circuiting

Handlers can choose to stop the chain after processing a request (short-circuiting), or allow it to continue. This is crucial for performance and correctness, especially when only one handler should process a request.

Example:

class StopOnHandledHandler extends Handler {
    public void handleRequest(String request) {
        if (canHandle(request)) {
            // handle and do NOT call next
        } else if (next != null) {
            next.handleRequest(request);
        }
    }
    private boolean canHandle(String request) { /* ... */ return false; }
}

Chain of Responsibility vs. Other Patterns

  • Command Pattern: Encapsulates a request as an object, but does not chain handlers.

  • Decorator Pattern: Adds behavior to objects, but does not pass requests along a chain.

  • Observer Pattern: Notifies multiple objects, but does not allow for request handling to stop at the first capable handler.

Chain of Responsibility vs. Decorator

  • Both Chain of Responsibility and Decorator have very similar class structures, relying on recursive composition to pass execution through a series of objects.

  • Key Differences:

    • Chain of Responsibility (CoR): Handlers can execute arbitrary operations independently of each other and can stop passing the request further at any point (short-circuiting). This means a handler can decide to handle a request and prevent further processing, or let it continue down the chain.

    • Decorator: Decorators extend the object's behavior while keeping it consistent with the base interface. Decorators are not allowed to break the flow of the request; each decorator must always delegate to the next. The flow is guaranteed to reach the base object, with each decorator only adding to or modifying the behavior, never stopping it.

  • In summary:

    • CoR is about independent processing and optional short-circuiting of requests.

    • Decorator is about layering enhancements, always passing the call through the entire chain to the base object.

Thread Safety and Concurrency

In multithreaded environments, ensure that handler state is thread-safe. Avoid shared mutable state or use synchronization where necessary.

Drawbacks in Large Systems

  • Performance: Very long chains can introduce latency.

  • Maintainability: Tracing the flow of a request can be difficult. Consider adding logging or tracing at each handler.

Real-World Use Cases

  • Web Framework Middleware: HTTP request processing in frameworks like Spring or Express.js.

  • Event Processing Pipelines: Logging, validation, and transformation steps.

  • UI Event Handling: GUI toolkits often use this pattern for event propagation.


Advantages & Disadvantages

AdvantagesDisadvantages
Loose Coupling: Sender and receiver are decoupled.No Guarantee of Handling: A request might go unhandled if no handler processes it.
Flexibility: Handlers can be added, removed, or reordered easily.Debugging Difficulty: Tracing the flow can be harder in long chains.
Responsibility Sharing: Multiple objects can handle a request.

Best Practices

  • Always provide a default handler at the end of the chain to catch unhandled requests.

  • Keep handlers focused on a single responsibility.

  • Avoid very long chains to reduce complexity.


Summary Table

FeatureDescription
Pattern TypeBehavioral
IntentDecouple sender and receiver of a request
StructureChain of handlers
FlexibilityHigh (add/remove/reorder handlers easily)
Real-World ExampleCustomer support escalation

Conclusion

The Chain of Responsibility pattern is a powerful tool for building flexible and maintainable systems. By passing requests along a chain of handlers, you can decouple the sender from the receiver, making your code easier to extend and modify.

Next time you need to process requests through multiple steps, consider using the Chain of Responsibility!


May your chains flow smoothly and your handlers always pass the right requests with style!

0
Subscribe to my newsletter

Read articles from Maverick directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Maverick
Maverick