Passing a request along a chain of handlers until one handles it — built through a request-validation pipeline, and how this is exactly the shape of Spring Security's own filter chain.
Published September 23, 2026
Rather than one class containing an if/else chain that checks every possible condition, Chain of Responsibility gives each check its own handler object, linked in sequence — each handler either processes the request and stops the chain, or passes it to the next handler.
abstract class RequestHandler {
protected RequestHandler next;
RequestHandler setNext(RequestHandler next) { this.next = next; return next; } // returns next — enables chaining the setup itself
final void handle(Request request) {
if (!process(request)) return; // this handler rejected the request — stop the chain here
if (next != null) next.handle(request); // passed — hand off to the next link
}
protected abstract boolean process(Request request); // true = continue chain, false = reject/stop
}
class AuthCheck extends RequestHandler {
protected boolean process(Request request) {
if (!request.isAuthenticated()) { reject(request, 401); return false; }
return true;
}
}
class RateLimitCheck extends RequestHandler {
protected boolean process(Request request) {
if (isRateLimited(request)) { reject(request, 429); return false; }
return true;
}
}
class SchemaCheck extends RequestHandler {
protected boolean process(Request request) {
if (!isValidSchema(request)) { reject(request, 400); return false; }
return true;
}
}
RequestHandler chain = new AuthCheck();
chain.setNext(new RateLimitCheck()).setNext(new SchemaCheck());
chain.handle(incomingRequest); // flows through Auth -> RateLimit -> Schema, stopping at the first rejection
Each handler is independently testable, independently addable/removable, and doesn't need to know anything about the other checks in the chain — AuthCheck has no idea RateLimitCheck even exists, it just calls next.handle() if it passes.
If this looks familiar, it should — it's structurally identical to the servlet filter chain covered in synchronized and Locks' broader context and, more directly, Spring Security's own SecurityFilterChain (see Spring Security Overview): each filter either processes the request and calls chain.doFilter() to pass it along, or short-circuits by writing a response directly and never calling doFilter(). Recognizing "this is Chain of Responsibility" the moment you see a filter/middleware/interceptor pipeline is a genuinely useful pattern-recognition shortcut — it tells you immediately how ordering matters, how a stage can short-circuit, and how to add a new stage without touching existing ones.
Q: What happens if no handler in the chain processes the request at all? A: In the implementation above, the request falls off the end silently (no explicit final handler) — production chains typically add a terminal "default handler" at the end that either accepts (if reaching the end implies success) or explicitly rejects, so there's no ambiguous silent-fallthrough case.
Q: Can more than one handler in the chain process the same request? A: Yes, depending on design — the version above stops the chain entirely on rejection but continues past every handler that accepts, meaning all three checks run in sequence for a valid request (not just the first one). A different variant could have exactly one handler "claim" and fully handle a request, stopping the chain on acceptance too — the pattern's core shape (link handlers, pass along) accommodates either policy depending on what the problem needs.
Q: How is Chain of Responsibility different from just calling three validation methods in sequence in one function? A: Functionally similar for a fixed, known set of checks — the pattern's value shows up when the set of handlers needs to be configured, reordered, or extended without modifying a central method (matching the Open/Closed Principle), or when handlers need to be assembled differently in different contexts (e.g. a different chain for public vs authenticated endpoints).
Q: Does the order of handlers in the chain matter here, and why? A: Yes, meaningfully — AuthCheck runs before RateLimitCheck here specifically so an unauthenticated request gets a 401 rather than consuming rate-limit budget it may not even be entitled to; a different ordering (rate-limit before auth) would let unauthenticated traffic exhaust the rate limiter, a real-world reason chain ordering is a deliberate design decision, not an arbitrary one.