Chain of Responsibility: Routing a Request Through Handlers
Pass a request along a chain of handlers until one processes it — the pattern behind servlet filters, middleware pipelines, and exception handling chains.
Chain of Responsibility
Pass a request along a chain of potential handlers until one of them handles it. Each handler independently decides: process this request, pass it to the next handler, or do both. The sender of the request doesn't know — and shouldn't need to know — which handler in the chain will ultimately deal with it, or how many handlers exist.
This is how middleware pipelines, approval-escalation workflows, and exception-handling chains all work: a linear sequence of "can I handle this? if not, pass it on."
1. The Problem: One Method Doing Every Check
A request-processing method that inlines every validation, auth, and logging concern becomes an ever-growing sequence that's impossible to reorder, reuse partially, or extend without editing the whole thing.
// VIOLATION: every concern hardcoded into one method, in a fixed, unchangeable order
class RequestProcessor {
void handle(Request request) {
// authentication
if (request.token() == null) {
throw new SecurityException("Missing token");
}
// logging
System.out.println("Request: " + request.path());
// rate limiting
if (isRateLimited(request.clientId())) {
throw new IllegalStateException("Rate limit exceeded");
}
// validation
if (request.body() == null || request.body().isBlank()) {
throw new IllegalArgumentException("Empty body");
}
// finally, the actual business logic
System.out.println("Processing: " + request.body());
}
private boolean isRateLimited(String clientId) { return false; }
}Reordering "rate limit before auth" means editing this method. Reusing just "logging + validation" for a different endpoint that doesn't need auth means copy-pasting two of the four blocks. Adding a fifth concern (CORS headers) means another edit to an already-long method.
2. Structure
Each handler holds a reference to the next handler. setNext() lets the chain be assembled — and reordered — entirely outside any handler's own code. A handler that decides not to pass the request forward simply doesn't call next.handle(request).
3. Full Implementation
record Request(String path, String token, String clientId, String body) {}
// The Handler contract: process, optionally delegate to the next link
abstract class Handler {
private Handler next;
Handler setNext(Handler next) {
this.next = next;
return next; // returned so chain assembly can be fluently chained: a.setNext(b).setNext(c)
}
void handle(Request request) {
if (next != null) {
next.handle(request);
}
// Base case: no next handler — the chain simply ends here.
}
}
class AuthHandler extends Handler {
@Override
void handle(Request request) {
if (request.token() == null) {
throw new SecurityException("Missing token");
}
System.out.println("[Auth] token OK");
super.handle(request); // pass to next link
}
}
class LoggingHandler extends Handler {
@Override
void handle(Request request) {
System.out.println("[Log] " + request.path());
super.handle(request);
}
}
class RateLimitHandler extends Handler {
private final Map<String, Integer> callCounts = new HashMap<>();
@Override
void handle(Request request) {
int count = callCounts.merge(request.clientId(), 1, Integer::sum);
if (count > 100) {
throw new IllegalStateException("Rate limit exceeded for " + request.clientId());
// Note: does NOT call super.handle() — the chain stops here on rejection
}
System.out.println("[RateLimit] OK (" + count + "/100)");
super.handle(request);
}
}
class ValidationHandler extends Handler {
@Override
void handle(Request request) {
if (request.body() == null || request.body().isBlank()) {
throw new IllegalArgumentException("Empty body");
}
System.out.println("[Validate] body OK");
super.handle(request);
}
}
class BusinessLogicHandler extends Handler {
@Override
void handle(Request request) {
System.out.println("[Business] processing: " + request.body());
// terminal handler — no super.handle() call needed, nothing follows it
}
}
class Demo {
public static void main(String[] args) {
Handler auth = new AuthHandler();
Handler logging = new LoggingHandler();
Handler rateLimit = new RateLimitHandler();
Handler validation = new ValidationHandler();
Handler business = new BusinessLogicHandler();
// Chain order assembled here — NOT hardcoded inside any handler
auth.setNext(logging).setNext(rateLimit).setNext(validation).setNext(business);
auth.handle(new Request("/orders", "tok-123", "client-1", "{\"item\":\"widget\"}"));
// Reusing just logging + validation for a different endpoint is now trivial:
Handler lightweightChain = new LoggingHandler();
lightweightChain.setNext(new ValidationHandler());
lightweightChain.handle(new Request("/health", null, "client-2", "ok"));
}
}Every handler is independently reusable, independently testable, and the order of the chain is assembled data — not hardcoded control flow inside a single method.
4. When to Use vs. When It's Overkill
| Use Chain of Responsibility when | Skip it when |
|---|---|
| Multiple handlers might process a request, but exactly which one (or how many) varies at runtime | There's a fixed, small, unchanging sequence of steps — a plain method call sequence is clearer |
| You want to add/remove/reorder processing steps without touching the others | The steps are tightly interdependent and can't sensibly be decoupled into standalone units |
| Building a pipeline/middleware where each stage is independently testable and reusable | Only one handler will ever process any given request — a direct call suffices |
| The set of handlers, or their order, needs to be configured/composed externally | Order is a hardcoded, permanent business rule with zero configuration need |
Over-applying CoR: building a chain for two fixed, always-both-run steps that will never be reordered, reused separately, or extended adds handler classes and setNext() wiring for a sequence a single method with two calls would express just as clearly.
5. Chain of Responsibility vs. Decorator
Both wrap a request/call through a sequence of objects with the same interface, which makes them easy to confuse structurally — but their intent is opposite:
| Chain of Responsibility | Decorator | |
|---|---|---|
| Who processes | Typically one handler processes and the chain may stop there | Every decorator in the chain runs, wrapping the call |
| Purpose | Routing — find the right handler for this request | Adding — layer additional behavior around a call, all of it |
| Can skip? | Yes — a handler can decline and pass to the next without doing its own work | No — each decorator always adds its behavior around the call |
| Chain can short-circuit | Yes — RateLimitHandler above stops the chain entirely on rejection | Rare — decorators generally always delegate onward |
| Typical use | Servlet filters (any filter can reject and stop), approval chains, error handlers | Adding logging/caching/compression around a fixed call |
A useful test: if you can remove a middle link and the request just skips it silently, or if any single link can stop the whole request outright — that's Chain of Responsibility. If every layer unconditionally contributes its behavior around the call, that's Decorator.
6. Chain of Responsibility vs. Command
Chain of Responsibility and Command are often taught together because both involve objects that "do something with a request," but they answer different questions:
| Chain of Responsibility | Command | |
|---|---|---|
| Question answered | Who should handle this request? | What action should be performed (and can it be undone/queued)? |
| Shape | A sequence of handlers, request flows through | A single encapsulated action, invoked by one invoker |
| Runtime flexibility | Which handler(s) engage varies per request | Which command runs is decided once by the invoker |
They compose naturally: a handler in a chain can internally execute a Command once it decides to process the request, keeping "should I handle this" (CoR's job) separate from "what exactly happens when I do" (Command's job).
7. Real-World / Production Examples
- Servlet filters (
javax.servlet.Filter) — the textbook CoR: each filter callschain.doFilter()to pass control onward, or stops the chain by not calling it (e.g., an auth filter rejecting a request). - Spring Security's filter chain — a configured sequence of security filters (CSRF, authentication, authorization, CORS), each deciding to continue or reject.
- Express.js / Koa middleware —
app.use(middleware)builds exactly this chain;next()is the "pass to the next handler" call. - Exception handling chains —
try { ... } catch (SpecificException e) { ... } catch (GeneralException e) { ... }is conceptually CoR: each catch clause is a handler that either processes the exception or lets it propagate to the next. - Logging frameworks (Logback/Log4j appender chains, filter chains) — a log event passes through a chain of filters, each deciding to allow, deny, or neutrally pass it on.
- Approval workflows — an expense request escalates through Manager → Director → VP, each approving within their limit or passing it up the chain.
Interview Questions
- Walk through how a request "declines" to be fully handled by one handler and moves to the next. What does the handler need to call?
- How does Chain of Responsibility let you reorder processing steps without touching any handler's internal code?
- Compare Chain of Responsibility with Decorator — both wrap a sequence of same-interface objects. What's the essential difference in intent?
- How do servlet filters implement Chain of Responsibility? What plays the role of "stop the chain"?
- Describe a scenario where a chain should be built dynamically (e.g., per-tenant configuration) rather than hardcoded at startup.
- What happens if no handler in the chain processes the request? How would you design the chain to guarantee something always handles it (a default/terminal handler)?
- How would you unit test a single handler in isolation, without constructing the entire chain?
- When would you choose a fixed sequence of method calls over building a Chain of Responsibility?