Design PatternsBehavioral
Chain of Responsibility Pattern
Pass a request along a chain of handlers, each deciding to process it, pass it on, or stop the chain.
Design Patterns
Intent
Chain of Responsibility decouples the sender of a request from its receivers by giving several objects a chance to handle it. Handlers are linked in a chain; each one either handles the request, passes it to the next handler, or both.
There are two common flavours:
- Classic GoF — exactly one handler processes the request (e.g. an approval escalates until someone has enough authority).
- Pipeline / filter chain — every handler does some work and decides whether to continue (e.g. servlet filters: auth → logging → rate limiting → controller).
Structure
sequenceDiagram
participant C as Client
participant A as AuthHandler
participant R as RateLimitHandler
participant V as ValidationHandler
participant H as BusinessHandler
C->>A: handle(request)
A->>R: next.handle(request)
R->>V: next.handle(request)
V-->>C: 400 Bad Request (chain stops)
Note over V,H: BusinessHandler is never reached
Example 1: expense approval (classic)
abstract class Approver {
private Approver next;
Approver then(Approver next) { this.next = next; return next; }
void approve(Expense e) {
if (canApprove(e)) System.out.println(getClass().getSimpleName() + " approved " + e);
else if (next != null) next.approve(e);
else System.out.println("Nobody can approve " + e);
}
abstract boolean canApprove(Expense e);
}
class TeamLead extends Approver { boolean canApprove(Expense e) { return e.amount() <= 500; } }
class Manager extends Approver { boolean canApprove(Expense e) { return e.amount() <= 5_000; } }
class Director extends Approver { boolean canApprove(Expense e) { return e.amount() <= 50_000; } }
Example 2: an HTTP filter pipeline
import java.util.*;
record Request(String path, Map<String, String> headers, String body) {}
record Response(int status, String body) {}
// Each filter receives the request and the rest of the chain
interface Filter {
Response doFilter(Request req, Chain chain);
}
// The chain walks the list of filters and finally calls the endpoint
final class Chain {
private final List<Filter> filters;
private final java.util.function.Function<Request, Response> endpoint;
private int index = 0;
Chain(List<Filter> filters, java.util.function.Function<Request, Response> endpoint) {
this.filters = filters;
this.endpoint = endpoint;
}
Response proceed(Request req) {
if (index < filters.size()) return filters.get(index++).doFilter(req, this);
return endpoint.apply(req);
}
}
class LoggingFilter implements Filter {
public Response doFilter(Request req, Chain chain) {
long start = System.nanoTime();
Response res = chain.proceed(req); // continue…
System.out.printf(" %s -> %d (%d µs)%n", req.path(), res.status(), (System.nanoTime() - start) / 1000);
return res; // …and post-process
}
}
class AuthFilter implements Filter {
public Response doFilter(Request req, Chain chain) {
String token = req.headers().get("Authorization");
if (token == null || !token.equals("Bearer secret")) return new Response(401, "Unauthorized"); // stop
return chain.proceed(req);
}
}
class ValidationFilter implements Filter {
public Response doFilter(Request req, Chain chain) {
if (req.body() == null || req.body().isBlank()) return new Response(400, "Body required");
return chain.proceed(req);
}
}
class Main {
static Response handle(Request req) {
var filters = List.<Filter>of(new LoggingFilter(), new AuthFilter(), new ValidationFilter());
return new Chain(filters, r -> new Response(201, "Created order from " + r.body())).proceed(req);
}
public static void main(String[] args) {
System.out.println(handle(new Request("/orders", Map.of("Authorization", "Bearer secret"), "{sku:42}")));
System.out.println(handle(new Request("/orders", Map.of(), "{sku:42}")));
System.out.println(handle(new Request("/orders", Map.of("Authorization", "Bearer secret"), "")));
}
}Output
Notice that LoggingFilter wraps the rest of the chain, so it can measure the total time and see the final status — even when a later filter short-circuits.
Where you’ll see it
- Servlet filters:
jakarta.servlet.Filter.doFilter(request, response, chain)— the model for the example above - Spring Security: the
SecurityFilterChainis a chain of ~15 filters (CORS, CSRF, authentication, authorization, exception translation) - Spring MVC
HandlerInterceptors and Spring Cloud GatewayGatewayFilters java.util.logging.Loggerpassing records to parent loggers- Exception handling:
try/catchblocks up the call stack are a built-in chain of responsibility
Trade-offs
| Pros | Cons |
|---|---|
| Add, remove or reorder handlers without touching the sender | No guarantee a request is handled at all |
| Each handler has a single responsibility | Long chains are hard to debug — which filter rejected it? |
| Handlers can be reused across chains | Ordering bugs (e.g. logging after auth hides failed logins) |
Finished this lesson?
Mark it complete to track your progress through Design Patterns.