LESSON 6.3
Chain of Responsibility
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request, chaining receiving objects sequentially.
15 min read
Real-world analogy
An automated customer support phone line or corporate purchase approval ladder illustrates the Chain of Responsibility. When you dial technical support, you first reach an automated voice menu for basic password resets. If your issue is unresolved, your call escalates to a Tier 1 support technician. If the bug requires code inspection, the technician escalates the call to Tier 2 Engineering. Each level either resolves your issue or forwards your call to the next tier in the chain.
The problem
You are building an HTTP API gateway. Every incoming request must undergo a sequence of security and infrastructure checks before reaching the business controller:
IPRateLimiter: Block client if requests exceed 100 per minute.AuthenticationFilter: Verify JWT signature and token expiration.RoleAuthorizer: Check that the authenticated user possesses theROLE_ADMINscope.PayloadSanitizer: Strip malicious<script>tags from input JSON.
Without the Chain of Responsibility, developers write a monolithic controller method filled with nested if-else blocks:
// Naive implementation: Monolithic nested conditional controller
class ApiGatewayController {
handleRequest(req) {
// 1. Rate limiting
if (this.isRateLimited(req.ip)) {
return { status: 429, error: "Too Many Requests" };
}
// 2. Authentication
if (!req.headers.authorization) {
return { status: 401, error: "Missing Auth Token" };
}
const user = this.verifyToken(req.headers.authorization);
if (!user) {
return { status: 403, error: "Invalid Token" };
}
// 3. Authorization
if (req.path.startsWith("/admin") && user.role !== "ADMIN") {
return { status: 403, error: "Unauthorized" };
}
// 4. Payload sanitization
req.body = this.sanitize(req.body);
return { status: 200, data: "Order Created" };
}
}public class Example {
static class ApiGatewayController {
// Monolithic: 5+ disparate concerns tangled in a single rigid method.
// Reordering checks, disabling checks in test environments, or adding
// new security layers requires modifying this brittle conditional tree.
public void handleRequest(String req) {
System.out.println("Processing monolithic nested checks...");
}
}
This monolithic design violates the Single Responsibility Principle and the Open/Closed Principle. It is impossible to reorder checks, add a new cache handler, or bypass authentication in local development tests without editing the monolithic method.
The solution
The Chain of Responsibility pattern decouples the sender of a request from its receivers by giving multiple handlers a chance to process the request. Handlers are linked into a sequential chain, and the request travels down the chain until a handler either fulfills it or passes it to the next link.
- Handler (
RequestHandler): Abstract class declaring thesetNext()link andhandle()contract. - Concrete Handlers (
RateLimiter,Authenticator,Authorizer): Inspects the request, processes it or forwards it tonext. - Client: Assembles the chain and dispatches the request to the head handler.
Step-by-step implementation
Step 1: Define the base handler abstraction
Establish the handler interface with a reference to the next handler in the pipeline.
Step 2: Implement concrete validation handlers
Each handler performs its specific check. If it fails, it halts and returns an error; otherwise, it calls super.handle(req).
Step 3: Assemble and execute the pipeline
The client configures the chain dynamically and passes incoming requests into the head.
const pipeline = new RateLimitHandler();
pipeline.setNext(new AuthTokenHandler());
const res = pipeline.handle({ ip: "10.0.0.1", token: "valid_jwt_token" });
console.log(`Response: ${res.status} ${res.message ?? res.error}`);public class Example
The complete example
Here is the complete, runnable implementation demonstrating a corporate expense approval chain (Stop Chain).
--- Request 1: Laptop Purchase ($800) ---
[Approved by Team Manager] Expense EXP-001 ($800) approved.
--- Request 2: Server Cluster Hardware ($7,500) ---
[Escalating] Team Manager cannot approve $7500. Escalating to next tier...
[Approved by Director] Expense EXP-002 ($7500) approved.
--- Request 3: Annual Enterprise Software License ($35,000) ---
[Escalating] Team Manager cannot approve $35000. Escalating to next tier...
[Escalating] Director cannot approve $35000. Escalating to next tier...
[Approved by Vice President] Expense EXP-003 ($35000) approved.
--- Request 4: Corporate Jet Charter ($150,000) ---
Variations
Stop chain vs pass-through middleware chain
- Stop Chain (Pure GoF): Exactly one handler satisfies the request and terminates the chain execution immediately (e.g. corporate expense approvals, exception handler catch blocks).
- Pass-Through / Interceptor Chain: Every handler in the chain performs work (authentication, CORS header injection, logging, telemetry), mutates or inspects the payload, and calls
next()to invoke the remainder of the pipeline (e.g. Express middleware, Java Servlet Filters).
Express.js middleware and Java Servlet filters
Modern web servers organize request handling as a Chain of Responsibility:
- Express.js:
(req, res, next) => { ... next(); } - Java Servlet Filters:
filterChain.doFilter(servletRequest, servletResponse)
Where you've already used it
- Java Servlet
FilterChain:javax.servlet.Filterwraps HTTP requests with compression, security checks, and encoding conversions before reaching the target servlet. - Spring Security
SecurityFilterChain: Executes a sequence of 15+ security filters (CSRF filter, UsernamePasswordAuthenticationFilter, BearerTokenAuthenticationFilter). - DOM Event Bubbling: In the browser, an event on a
<button>travels up through parent<div>,<form>,<body>, anddocumentnodes until a listener callsevent.stopPropagation(). - Netty
ChannelPipeline: Decodes incoming network bytes into frames, parses SSL/TLS, and extracts domain messages across linked pipeline handlers.
Pros and cons
Advantages
- Control Order of Processing: Handlers can be rearranged, inserted, or omitted dynamically at runtime.
- Single Responsibility Principle: Each handler focuses exclusively on a single validation, transformation, or approval step.
- Open/Closed Principle: Add new security checks or handlers without touching existing handler code.
Disadvantages
- Uncaught Requests: If no handler can process the request, it can silently fall off the end of the chain.
- Performance Latency: Deep chains with dozens of handlers introduce stack depth overhead and latency.
- Difficult to Trace: Tracing execution across 20 dynamic handlers can complicate debugging.
When not to use it
Do not use Chain of Responsibility when all checks must execute regardless of failure and their execution order does not matter. If you just need a list of independent validators that all run to produce a combined list of validation errors, a simple loop over a list of validators is cleaner.
Related patterns
| Pattern | Execution Model | Intent |
|---|---|---|
| Chain of Responsibility | Sequential links | Passes request along until handled |
| Command | Encapsulated execution | Packages request as a standalone object |
| Decorator | Recursive wrappers | Enhances behavior transparently (all layers run) |
| Composite | Part-whole tree | Propagates requests upward from leaf to parents |
Interview questions
How does Chain of Responsibility differ from Decorator? Both rely on recursive delegation through a shared interface. However, Decorator executes all layers to enhance functionality, whereas classic Chain of Responsibility stops processing as soon as a single handler satisfies the request.
What happens if a request is not handled by any link in the chain? The request falls off the end unhandled. In production systems, you should always append a default fallback handler at the tail of the chain to log a warning, throw an exception, or return a 404/400 response.
How does DOM event bubbling illustrate Chain of Responsibility?
When an event occurs on a deeply nested DOM element, the browser gives that element the first chance to handle it. If not stopped via event.stopPropagation(), the event travels up through ancestor container elements in a chain of responsibility.
Can a handler in a Chain of Responsibility mutate the request object before passing it to the next handler? Yes. In modern interceptor/middleware pipelines (like Express or Spring Web filters), handlers frequently mutate the request (e.g. decrypting bodies, attaching authenticated user identities, or parsing query strings).
Try it
Rate Limiter LLD
Design a token bucket with an injectable clock, atomic state, and useful retry hints.