Chain of Responsibility Pattern explained simply
When a request needs to pass through multiple handlers and each handler decides whether to process it or pass it along, you have the Chain of Responsibility Pattern.
Think of it like an approval pipeline. A request enters at one end, and each handler in the chain either deals with it, rejects it, or forwards it to the next handler. The sender never knows which handler will ultimately process the request – and it shouldn’t have to.
What is the Chain of Responsibility Pattern?
At its core:
- Decouple the sender of a request from its receivers.
- Give more than one object a chance to handle the request.
- Pass the request along a chain of handlers until one of them handles it.
Each handler in the chain holds a reference to the next handler. When a request comes in, the handler either processes it or passes it down the line. The client only talks to the first handler – it has no idea how many handlers exist or which one will ultimately do the work.
One request, many potential handlers.
Here is a UML class diagram showing how the pieces fit together. Each handler extends RequestHandler, holds a reference to the next handler in the chain, and either processes the request or delegates it forward.
<mxfile>
<diagram name="Chain of Responsibility Pattern">
<mxGraphModel dx="1100" dy="780" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1100" pageHeight="700">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<!-- RequestHandler abstract class -->
<mxCell id="2" value="<<abstract>>
RequestHandler
─────────────────
- nextHandler: RequestHandler
+ setNext(handler): RequestHandler
+ handle(request: HttpRequest): HttpResponse" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" parent="1">
<mxGeometry x="350" y="30" width="350" height="100" as="geometry"/>
</mxCell>
<!-- RateLimitHandler -->
<mxCell id="3" value="RateLimitHandler
─────────────────
+ handle(request): HttpResponse" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="30" y="230" width="230" height="70" as="geometry"/>
</mxCell>
<!-- AuthenticationHandler -->
<mxCell id="4" value="AuthenticationHandler
─────────────────
+ handle(request): HttpResponse" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="280" y="230" width="240" height="70" as="geometry"/>
</mxCell>
<!-- AuthorizationHandler -->
<mxCell id="5" value="AuthorizationHandler
─────────────────
+ handle(request): HttpResponse" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="540" y="230" width="240" height="70" as="geometry"/>
</mxCell>
<!-- ValidationHandler -->
<mxCell id="6" value="ValidationHandler
─────────────────
+ handle(request): HttpResponse" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#f8cecc;strokeColor=#b85450;" vertex="1" parent="1">
<mxGeometry x="800" y="230" width="230" height="70" as="geometry"/>
</mxCell>
<!-- Client -->
<mxCell id="7" value="Client" style="shape=rectangle;whiteSpace=wrap;html=1;align=center;fontSize=12;fontFamily=monospace;fillColor=#fff2cc;strokeColor=#d6b656;" vertex="1" parent="1">
<mxGeometry x="30" y="50" width="120" height="50" as="geometry"/>
</mxCell>
<!-- Client uses RequestHandler -->
<mxCell id="8" value="uses" style="endArrow=open;endSize=12;dashed=1;strokeColor=#d6b656;" edge="1" source="7" target="2" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- Self-reference: nextHandler -->
<mxCell id="9" value="nextHandler" style="endArrow=open;endSize=12;dashed=0;strokeColor=#6c8ebf;exitX=1;exitY=0.3;exitDx=0;exitDy=0;entryX=1;entryY=0.7;entryDx=0;entryDy=0;" edge="1" source="2" target="2" parent="1">
<mxGeometry relative="1" as="geometry">
<Array as="points">
<mxPoint x="740" y="60"/>
<mxPoint x="740" y="100"/>
</Array>
</mxGeometry>
</mxCell>
<!-- RateLimitHandler extends RequestHandler -->
<mxCell id="10" style="endArrow=block;endFill=0;dashed=1;strokeColor=#b85450;" edge="1" source="3" target="2" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- AuthenticationHandler extends RequestHandler -->
<mxCell id="11" style="endArrow=block;endFill=0;dashed=1;strokeColor=#b85450;" edge="1" source="4" target="2" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- AuthorizationHandler extends RequestHandler -->
<mxCell id="12" style="endArrow=block;endFill=0;dashed=1;strokeColor=#b85450;" edge="1" source="5" target="2" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
<!-- ValidationHandler extends RequestHandler -->
<mxCell id="13" style="endArrow=block;endFill=0;dashed=1;strokeColor=#b85450;" edge="1" source="6" target="2" parent="1">
<mxGeometry relative="1" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
Real-Life Analogy
Picture an expense approval process at a company. An employee submits an expense report for $8,000.
First, it hits their direct manager. The manager can approve anything under $1,000, so this one is above their pay grade. They pass it up.
Next, it reaches the department director. The director can approve up to $5,000. Still too much. Passed along again.
Finally, it lands on the VP’s desk. The VP can approve up to $50,000. Done – approved and processed.
The employee didn’t need to know the approval hierarchy. They just submitted the request, and the chain figured out who should handle it. If the expense was $500, the manager would have approved it right there and the director and VP would never even see it.
That is the Chain of Responsibility Pattern. Each handler knows its own limits and either handles the request or passes it to the next person in line.
Structure
- Handler (
RequestHandler): An abstract class that defines the interface for handling requests and holds a reference to the next handler in the chain. - Concrete Handlers (
RateLimitHandler,AuthenticationHandler,AuthorizationHandler,ValidationHandler): Each implements its own handling logic and decides whether to process the request or pass it along. - Client: Sends the request to the first handler in the chain. It doesn’t know or care which handler will ultimately process it.
Java Example: Authentication and Authorization Middleware Chain
This is the pattern behind Spring Security filter chains, Express.js middleware stacks, and Java Servlet filters. Let’s build a realistic middleware pipeline from scratch.
First, some simple models for HTTP requests and responses:
import java.util.Map;
public class HttpRequest {
private final String path;
private final String method;
private final Map<String, String> headers;
private final String body;
private final String clientIp;
public HttpRequest(String path, String method, Map<String, String> headers,
String body, String clientIp) {
this.path = path;
this.method = method;
this.headers = headers;
this.body = body;
this.clientIp = clientIp;
}
public String getPath() { return path; }
public String getMethod() { return method; }
public String getHeader(String key) { return headers.get(key); }
public String getBody() { return body; }
public String getClientIp() { return clientIp; }
}
public class HttpResponse {
private final int statusCode;
private final String body;
public HttpResponse(int statusCode, String body) {
this.statusCode = statusCode;
this.body = body;
}
public int getStatusCode() { return statusCode; }
public String getBody() { return body; }
@Override
public String toString() {
return statusCode + " - " + body;
}
}
Now the abstract RequestHandler. This is the backbone of the chain. Every handler extends this class and gets automatic forwarding to the next handler:
public abstract class RequestHandler {
private RequestHandler nextHandler;
public RequestHandler setNext(RequestHandler next) {
this.nextHandler = next;
return next;
}
public HttpResponse handle(HttpRequest request) {
if (nextHandler != null) {
return nextHandler.handle(request);
}
// end of chain -- no handler processed the request
return new HttpResponse(200, "OK");
}
}
The setNext() method returns the next handler, which lets you chain the setup calls together. You will see that in a moment.
Now let’s build the concrete handlers. Each one checks a specific condition and either rejects the request or passes it along.
RateLimitHandler – checks if the client IP has exceeded the rate limit:
import java.util.HashMap;
import java.util.Map;
public class RateLimitHandler extends RequestHandler {
private final Map<String, Integer> requestCounts = new HashMap<>();
private final int maxRequests;
public RateLimitHandler(int maxRequestsPerMinute) {
this.maxRequests = maxRequestsPerMinute;
}
@Override
public HttpResponse handle(HttpRequest request) {
String ip = request.getClientIp();
int count = requestCounts.getOrDefault(ip, 0) + 1;
requestCounts.put(ip, count);
if (count > maxRequests) {
System.out.println("[RATE LIMIT] Blocked " + ip
+ " (" + count + " requests)");
return new HttpResponse(429, "Too Many Requests");
}
System.out.println("[RATE LIMIT] " + ip + " OK (" + count
+ "/" + maxRequests + ")");
return super.handle(request);
}
}
AuthenticationHandler – validates the JWT token from the Authorization header:
public class AuthenticationHandler extends RequestHandler {
@Override
public HttpResponse handle(HttpRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
System.out.println("[AUTH] No valid token found");
return new HttpResponse(401, "Unauthorized: missing or invalid token");
}
String token = authHeader.substring(7);
// In a real app, you would decode and verify the JWT here.
// For this example, we just check that it's not empty.
if (token.isEmpty()) {
System.out.println("[AUTH] Empty token");
return new HttpResponse(401, "Unauthorized: empty token");
}
System.out.println("[AUTH] Token validated successfully");
return super.handle(request);
}
}
AuthorizationHandler – checks if the user has the required role or permission:
import java.util.Set;
public class AuthorizationHandler extends RequestHandler {
private final Set<String> allowedRoles;
public AuthorizationHandler(Set<String> allowedRoles) {
this.allowedRoles = allowedRoles;
}
@Override
public HttpResponse handle(HttpRequest request) {
// In a real app, you would extract the role from the decoded JWT.
// Here we simulate it with a custom header.
String userRole = request.getHeader("X-User-Role");
if (userRole == null || !allowedRoles.contains(userRole)) {
System.out.println("[AUTHZ] Access denied for role: " + userRole);
return new HttpResponse(403,
"Forbidden: insufficient permissions");
}
System.out.println("[AUTHZ] Role '" + userRole + "' authorized");
return super.handle(request);
}
}
ValidationHandler – validates the request body and parameters:
public class ValidationHandler extends RequestHandler {
@Override
public HttpResponse handle(HttpRequest request) {
if ("POST".equals(request.getMethod())
|| "PUT".equals(request.getMethod())) {
String body = request.getBody();
if (body == null || body.isBlank()) {
System.out.println("[VALIDATION] Empty request body");
return new HttpResponse(400,
"Bad Request: request body cannot be empty");
}
}
System.out.println("[VALIDATION] Request is valid");
return super.handle(request);
}
}
Notice the pattern. Every handler follows the same structure: check a condition, reject if it fails, call super.handle(request) to pass it to the next handler if it passes. Clean and predictable.
Wiring Up the Chain
Now let’s connect the handlers and send some requests through.
import java.util.Map;
import java.util.Set;
public class MainProgram {
public static void main(String[] args) {
// Build the chain: RateLimit -> Auth -> Authz -> Validation
RateLimitHandler rateLimiter = new RateLimitHandler(5);
AuthenticationHandler authenticator = new AuthenticationHandler();
AuthorizationHandler authorizer =
new AuthorizationHandler(Set.of("admin", "editor"));
ValidationHandler validator = new ValidationHandler();
rateLimiter
.setNext(authenticator)
.setNext(authorizer)
.setNext(validator);
// --- Request 1: Valid admin POST ---
System.out.println("=== Request 1: Valid admin POST ===");
HttpRequest validRequest = new HttpRequest(
"/api/articles", "POST",
Map.of("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.abc",
"X-User-Role", "admin"),
"{\"title\": \"Chain of Responsibility\"}",
"192.168.1.10"
);
HttpResponse response1 = rateLimiter.handle(validRequest);
System.out.println("Result: " + response1);
// --- Request 2: Missing auth token ---
System.out.println("\n=== Request 2: No auth token ===");
HttpRequest noAuthRequest = new HttpRequest(
"/api/articles", "GET",
Map.of(),
"",
"192.168.1.20"
);
HttpResponse response2 = rateLimiter.handle(noAuthRequest);
System.out.println("Result: " + response2);
// --- Request 3: Wrong role ---
System.out.println("\n=== Request 3: Unauthorized role ===");
HttpRequest wrongRoleRequest = new HttpRequest(
"/api/articles", "DELETE",
Map.of("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.xyz",
"X-User-Role", "viewer"),
"",
"192.168.1.30"
);
HttpResponse response3 = rateLimiter.handle(wrongRoleRequest);
System.out.println("Result: " + response3);
// --- Request 4: Empty body on POST ---
System.out.println("\n=== Request 4: Empty POST body ===");
HttpRequest emptyBodyRequest = new HttpRequest(
"/api/articles", "POST",
Map.of("Authorization", "Bearer eyJhbGciOiJIUzI1NiJ9.def",
"X-User-Role", "editor"),
"",
"192.168.1.40"
);
HttpResponse response4 = rateLimiter.handle(emptyBodyRequest);
System.out.println("Result: " + response4);
}
}
Output:
=== Request 1: Valid admin POST ===
[RATE LIMIT] 192.168.1.10 OK (1/5)
[AUTH] Token validated successfully
[AUTHZ] Role 'admin' authorized
[VALIDATION] Request is valid
Result: 200 - OK
=== Request 2: No auth token ===
[RATE LIMIT] 192.168.1.20 OK (1/5)
[AUTH] No valid token found
Result: 401 - Unauthorized: missing or invalid token
=== Request 3: Unauthorized role ===
[RATE LIMIT] 192.168.1.30 OK (1/5)
[AUTH] Token validated successfully
[AUTHZ] Access denied for role: viewer
Result: 403 - Forbidden: insufficient permissions
=== Request 4: Empty POST body ===
[RATE LIMIT] 192.168.1.40 OK (1/5)
[AUTH] Token validated successfully
[AUTHZ] Role 'editor' authorized
[VALIDATION] Empty request body
Result: 400 - Bad Request: request body cannot be empty
Each request gets rejected at a different stage. Request 1 passes through the entire chain. Request 2 dies at authentication. Request 3 clears auth but fails authorization. Request 4 makes it all the way to validation before getting rejected. The chain does its job – each handler only worries about its own responsibility.
Key Components
- Handler: Declares the interface for handling requests. Optionally implements the default chaining behavior.
- ConcreteHandler: Handles the requests it is responsible for. Can access its successor. If it can handle the request, it does so; otherwise, it forwards the request.
- Client: Initiates the request to a ConcreteHandler in the chain. The client doesn’t know which handler will process the request.
When to Use the Chain of Responsibility Pattern
- When more than one object may handle a request, and the handler isn’t known in advance.
- When you want to issue a request to one of several objects without specifying the receiver explicitly.
- When the set of handlers should be configured dynamically at runtime.
- When you have a pipeline of processing steps where each step might short-circuit the flow.
Advantages and Disadvantages
Advantages:
- Decouples sender and receiver: The client doesn’t need to know the chain’s structure or which handler processes the request.
- Flexible chain configuration: You can add, remove, or reorder handlers at runtime without changing the client code.
- Single Responsibility Principle: Each handler focuses on one concern. Rate limiting doesn’t know about authentication. Authentication doesn’t know about validation.
- Open/Closed Principle: You can introduce new handlers without modifying existing ones.
Disadvantages:
- No guarantee of handling: If no handler in the chain processes the request, it might fall through silently. You need a fallback strategy.
- Debugging can be tricky: When a request passes through multiple handlers, tracing where something went wrong requires looking at the entire chain.
- Performance overhead: A long chain means the request passes through many handlers even if the first one could have handled it. For most applications this is negligible, but it is worth keeping in mind.
Real-World Use Cases
This pattern is baked into many frameworks and systems you use daily:
- Java Servlet Filters: Each filter in the chain calls
chain.doFilter(request, response)to pass the request to the next filter. This is literally the Chain of Responsibility Pattern. - Spring Security Filter Chain: Spring Security builds an entire security pipeline as a chain of filters – CORS, CSRF, authentication, authorization, session management – each one deciding whether to pass the request along or reject it.
- Express.js Middleware: Every
app.use()call adds a handler to the chain. Each middleware callsnext()to pass control to the next one. If it doesn’t callnext(), the chain stops. - Logging Pipelines: Log messages pass through a chain of handlers based on severity level. DEBUG handlers catch everything, ERROR handlers only catch errors.
- Exception Handling: Try-catch chains in many languages work the same way. An exception bubbles up through nested catch blocks until one handles it.
- Event Bubbling in UI Frameworks: When you click a button in the DOM, the click event bubbles up from the button to its parent div, then to the body, then to the document – each element gets a chance to handle it.
Wrapping Up
The Chain of Responsibility Pattern lets you build flexible, decoupled processing pipelines where each handler focuses on a single concern and decides whether to handle the request or pass it along. The client never needs to know the chain’s internal structure.
If you have worked with any web framework, you have already used this pattern. Servlet filters, Express middleware, Spring Security – they all follow the same idea. A request enters the chain, flows through as many handlers as needed, and gets either processed or rejected along the way.
The next time you find yourself writing a bunch of if-else checks in a single method to handle different conditions, consider breaking them into a chain of handlers instead. Your code will be cleaner, easier to test, and much simpler to extend.