Chain of Responsibility


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 theAuthInterceptor
(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
Advantages | Disadvantages |
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
Feature | Description |
Pattern Type | Behavioral |
Intent | Decouple sender and receiver of a request |
Structure | Chain of handlers |
Flexibility | High (add/remove/reorder handlers easily) |
Real-World Example | Customer 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!
Subscribe to my newsletter
Read articles from Maverick directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
