Chapter 05 · Article 27 of 55
Chain of Responsibility Pattern
Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request al…
Article outline15 sections on this page+
Overview
Intent: Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
Category: Behavioural Design Pattern
Also Known As: Chain of Command, Responsibility Chain
The Chain of Responsibility pattern decouples request senders from receivers by allowing multiple objects to handle a request without the sender knowing which object will ultimately process it. Each handler in the chain either processes the request or forwards it to the next handler, creating a flexible pipeline of potential processors.
The pattern promotes loose coupling and adheres to the Single Responsibility Principle - each handler focuses solely on its own processing logic and the decision of whether to pass the request along.
Problem It Solves
Without this pattern, request handling often devolves into rigid, tightly-coupled structures:
- Hard-coded request handling: The sender must know exactly which object handles which type of request, creating direct dependencies between sender and all possible receivers.
- Monolithic if-else/switch chains: A single method accumulates branching logic for every possible request type, violating the Open/Closed Principle. Adding a new handler requires modifying existing code.
- Inflexible processing pipelines: When multiple processors must act on a request in sequence (validation, authentication, logging), embedding them in a fixed order makes reordering or skipping steps difficult.
- God objects: One class becomes responsible for routing all requests, growing into an unmaintainable monolith.
// Anti-pattern: monolithic handler
function handleRequest(request) {
if (request.type == "auth") { /* auth logic */ }
else if (request.type == "rate") { /* rate limit logic */ }
else if (request.type == "log") { /* logging logic */ }
else if (request.type == "data") { /* business logic */ }
// Every new concern = modify this function
}
The Chain of Responsibility eliminates this by distributing handling logic across independent, composable handler objects.
When to Use / When NOT to Use
| When to Use | When NOT to Use |
|---|---|
| Multiple objects may handle a request, and the handler isn't known a priori | A single, well-known handler always processes the request |
| You want to issue a request without specifying the receiver explicitly | The processing order must be strictly guaranteed and never change |
| The set of handlers should be configurable dynamically at runtime | Every request MUST be handled (unless you add a fallback) |
| You need a processing pipeline (middleware, filters) | Performance is critical and chain traversal overhead is unacceptable |
| You want to decouple sender from receiver | The chain would contain only one handler (over-engineering) |
| Request handling logic changes frequently or varies by context | Handlers need bidirectional communication with the sender |
| You want to apply the Open/Closed Principle to request processing | You need guaranteed response time (long chains add latency) |
Key Concepts & Theory
Handler Interface
Defines a common interface with two responsibilities:
handle(request)- Process the request or delegate to the next handler.setNext(handler)- Establish the successor link in the chain.
Successor Chain
Each handler holds a reference to the next handler. This forms a singly-linked list of processors. The client only knows about the first handler in the chain.
Request Propagation
When a handler receives a request, it decides:
- Handle it - Process and (optionally) stop propagation.
- Pass it along - Forward to the next handler in the chain.
- Both - Process and still forward (impure chain).
Pure Chain vs Impure Chain
- Pure Chain: Exactly one handler processes the request. Once handled, propagation stops. Example: exception handling - only the first matching catch block executes.
- Impure Chain: Multiple handlers may process the same request. Each handler decides independently whether to process AND whether to forward. Example: servlet filters - every filter in the chain processes the request.
Base Handler (Template)
A common abstract base class implements the chaining mechanics (storing the next handler, forwarding logic), so concrete handlers only implement their specific processing logic.
ASCII Class Diagram
┌─────────────────────────────────────────────────────────────────────┐
│ CLIENT │
│ - Builds the chain │
│ - Sends request to first handler │
└──────────────────────────────┬──────────────────────────────────────┘
│ sends request
▼
┌──────────────────────────────────────────────────────────────┐
│ <<interface>> Handler │
├──────────────────────────────────────────────────────────────┤
│ + setNext(handler: Handler): Handler │
│ + handle(request: Request): Result │
└──────────────────────────────┬───────────────────────────────┘
│ implements
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ ConcreteHandlerA │ │ ConcreteHandlerB │ │ ConcreteHandlerC │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ - next: Handler │ │ - next: Handler │ │ - next: Handler │
├──────────────────┤ ├──────────────────┤ ├──────────────────┤
│ + setNext() │ │ + setNext() │ │ + setNext() │
│ + handle() │ │ + handle() │ │ + handle() │
└────────┬─────────┘ └────────┬─────────┘ └──────────────────┘
│ next │ next
└────────────────────►└──────────────────────►
Chain: A ──next──► B ──next──► C ──next──► null
Pseudocode Implementation
Example 1: Support Ticket Escalation (Pure Chain)
Each support level handles tickets up to a certain severity. If it cannot handle, it escalates to the next level.
enum Severity { LOW, MEDIUM, HIGH, CRITICAL }
interface SupportHandler {
setNext(handler: SupportHandler): SupportHandler
handle(ticket: Ticket): string
}
abstract class BaseSupportHandler implements SupportHandler {
private next: SupportHandler = null
setNext(handler: SupportHandler): SupportHandler {
this.next = handler
return handler // enables chaining: a.setNext(b).setNext(c)
}
handle(ticket: Ticket): string {
if (this.next != null) {
return this.next.handle(ticket)
}
return "No handler available for ticket: " + ticket.id
}
}
class Level1Support extends BaseSupportHandler {
handle(ticket: Ticket): string {
if (ticket.severity == LOW) {
return "Level1: Resolved ticket " + ticket.id + " (password reset, FAQ)"
}
return super.handle(ticket) // escalate
}
}
class Level2Support extends BaseSupportHandler {
handle(ticket: Ticket): string {
if (ticket.severity == MEDIUM) {
return "Level2: Resolved ticket " + ticket.id + " (config issue, bug)"
}
return super.handle(ticket)
}
}
class Manager extends BaseSupportHandler {
handle(ticket: Ticket): string {
if (ticket.severity == HIGH) {
return "Manager: Resolved ticket " + ticket.id + " (escalated complaint)"
}
return super.handle(ticket)
}
}
class Director extends BaseSupportHandler {
handle(ticket: Ticket): string {
if (ticket.severity == CRITICAL) {
return "Director: Resolved ticket " + ticket.id + " (system outage, legal)"
}
return super.handle(ticket)
}
}
// Client builds the chain
level1 = new Level1Support()
level1.setNext(new Level2Support())
.setNext(new Manager())
.setNext(new Director())
// Usage
print(level1.handle(Ticket(id=101, severity=LOW))) // Level1 handles
print(level1.handle(Ticket(id=102, severity=CRITICAL))) // Escalates to Director
Example 2: Middleware Pipeline (Impure Chain)
Each middleware processes the request AND passes it to the next handler. Any middleware can short-circuit the chain (e.g., auth failure).
interface Middleware {
setNext(middleware: Middleware): Middleware
process(request: Request, response: Response): void
}
abstract class BaseMiddleware implements Middleware {
private next: Middleware = null
setNext(middleware: Middleware): Middleware {
this.next = middleware
return middleware
}
processNext(request: Request, response: Response): void {
if (this.next != null) {
this.next.process(request, response)
}
}
}
class AuthMiddleware extends BaseMiddleware {
process(request: Request, response: Response): void {
if (!request.hasValidToken()) {
response.status = 401
response.body = "Unauthorized"
return // short-circuit: do NOT call processNext
}
request.user = decodeToken(request.token)
this.processNext(request, response) // continue chain
}
}
class RateLimitMiddleware extends BaseMiddleware {
process(request: Request, response: Response): void {
if (rateLimiter.isExceeded(request.ip)) {
response.status = 429
response.body = "Too Many Requests"
return // short-circuit
}
this.processNext(request, response)
}
}
class LoggingMiddleware extends BaseMiddleware {
process(request: Request, response: Response): void {
log("Incoming: " + request.method + " " + request.path)
this.processNext(request, response)
log("Outgoing: " + response.status) // post-processing
}
}
class RequestHandler extends BaseMiddleware {
process(request: Request, response: Response): void {
response.status = 200
response.body = businessLogic(request)
// Terminal handler - does not call processNext
}
}
// Client assembles the pipeline
pipeline = new AuthMiddleware()
pipeline.setNext(new RateLimitMiddleware())
.setNext(new LoggingMiddleware())
.setNext(new RequestHandler())
pipeline.process(incomingRequest, new Response())
Pure Chain vs Impure Chain
| Aspect | Pure Chain | Impure Chain |
|---|---|---|
| Handlers that process | Exactly one | One or more (potentially all) |
| After handling | Propagation stops | Propagation may continue |
| Guarantee | At most one handler acts | Multiple handlers may act |
| Short-circuiting | Implicit (handler consumes request) | Explicit (handler decides not to forward) |
| Use case | Event handling, exception catching, escalation | Middleware pipelines, filter chains, logging |
| Example | Support ticket escalation, ATM dispensing | Servlet filters, Express.js middleware |
| Risk | Request may go unhandled | Ordering bugs, duplicate processing |
| Control flow | Linear, first-match-wins | Linear, all-match-process |
Real-World Examples
1. Servlet Filters (Java EE)
The FilterChain in Java Servlet specification is a textbook impure chain. Each Filter calls chain.doFilter(request, response) to pass control to the next filter. Filters handle authentication, CORS, compression, and logging.
2. Middleware Stacks (Express.js / Django)
Express.js middleware functions receive (req, res, next). Calling next() passes to the next middleware. Django's middleware classes implement process_request and process_response hooks in a layered chain.
3. Exception Handling (try-catch blocks)
Multiple catch blocks form a pure chain. The runtime walks the chain top-to-bottom; the first matching catch block handles the exception, and propagation stops.
4. Event Bubbling in DOM
When a click event fires on a nested element, it bubbles up through parent elements. Each element's event listener can handle the event and optionally call stopPropagation() to halt the chain.
5. Approval Workflows
Purchase order approvals: amounts under $1,000 are approved by a team lead, under $10,000 by a department head, under $100,000 by a VP, and above by the CFO. Each level checks its authority threshold.
6. Logging Frameworks
Log4j/SLF4J appenders form chains. A log message passes through multiple appenders (console, file, remote) - each decides independently whether to process based on log level.
7. ATM Cash Dispensing
Dispense $100 bills first, then $50, then $20, then $10. Each denomination handler dispenses what it can and passes the remainder to the next.
Chain of Responsibility vs Decorator vs Composite
| Aspect | Chain of Responsibility | Decorator | Composite |
|---|---|---|---|
| Intent | Pass request until one (or more) handlers process it | Add behaviour to an object dynamically | Treat individual and composite objects uniformly |
| Structure | Linear linked list of handlers | Nested wrappers around a core object | Tree hierarchy |
| Flow control | Handler can stop or continue the chain | Always delegates to the wrapped object | Recursively traverses children |
| Who processes | One handler (pure) or many (impure) | All decorators + core object | All nodes in subtree |
| Coupling | Handlers are independent, unaware of chain structure | Each decorator knows its wrappee | Parent knows children |
| Termination | When a handler consumes the request or chain ends | Always reaches the core component | Leaf nodes terminate recursion |
| Example | Middleware pipeline | BufferedInputStream wrapping FileInputStream | File system (files + directories) |
| Relationship | Handlers are peers | Decorators wrap a single subject | Parent-child hierarchy |
Advantages & Disadvantages
| Advantages | Disadvantages |
|---|---|
| Decouples sender and receiver - sender doesn't know who handles | No guarantee of handling - request may fall off the chain unprocessed |
| Open/Closed Principle - add new handlers without modifying existing ones | Debugging difficulty - hard to trace which handler processed a request |
| Single Responsibility - each handler has one concern | Performance overhead - long chains add latency from traversal |
| Dynamic configuration - chain can be assembled/modified at runtime | Ordering sensitivity - incorrect handler order produces wrong behaviour |
| Flexible processing - easily reorder, add, or remove handlers | Runtime errors - misconfigured chains may cause infinite loops or dropped requests |
| Reduces conditional complexity - eliminates large if-else blocks | Complexity - simple scenarios don't warrant the pattern overhead |
| Reusability - handlers can be shared across different chains | Testing - integration testing requires assembling chains |
Constraints & Edge Cases
Unhandled Requests
If no handler in the chain matches, the request silently disappears. Mitigations:
- Add a fallback/default handler at the end of the chain that logs or throws.
- Return a sentinel value indicating "not handled" so the client can react.
Circular Chains
If handler A points to B, B points to C, and C points back to A, the chain loops infinitely. Mitigations:
- Validate chain construction (detect cycles via visited-set).
- Set a maximum hop count; abort after N forwards.
Performance with Long Chains
Each handler adds a method call and conditional check. For latency-sensitive paths:
- Keep chains short (< 10 handlers).
- Profile and consider collapsing rarely-separated handlers.
- Use indexed dispatch (HashMap) if the routing key is known upfront.
Dynamic Chain Modification
Modifying the chain while a request is in-flight can cause skipped or repeated handlers. Mitigations:
- Use immutable chain snapshots per request.
- Apply modifications only between requests (double-buffering).
Thread Safety
In concurrent systems, shared mutable chain references require synchronization. Consider thread-local chains or copy-on-write semantics.
Interview Follow-ups
Q1: How would you ensure every request gets handled in a Chain of Responsibility?
A: Add a terminal/fallback handler at the end of the chain that always processes the request - either by returning a default response, logging an error, or throwing an exception. This guarantees no request silently falls off the chain. In middleware pipelines, the final handler is typically the actual request processor (controller/route handler).
Q2: How does Chain of Responsibility differ from the Observer pattern?
A: In Chain of Responsibility, a request travels sequentially through handlers until one (or more) processes it - it's a one-to-one-of-many relationship. In Observer, a notification is broadcast to ALL subscribers simultaneously - it's a one-to-many relationship. CoR is sequential and conditional; Observer is parallel and unconditional.
Q3: Can you combine Chain of Responsibility with other patterns?
A: Yes, commonly:
- With Composite: Handlers can be composite objects whose children form sub-chains.
- With Command: The request object passed through the chain can be a Command, encapsulating all context needed for processing.
- With Decorator: Decorators always forward; CoR handlers may stop. They can coexist when some chain links always process (decorator-like) and others conditionally stop.
Hint-Only Questions
Q4: How would you implement priority-based handling where certain handlers get first chance regardless of chain order?
Hints:
- Think about sorting handlers by priority before assembling the chain.
- Consider a dispatcher that maintains a priority queue of handlers rather than a fixed linked list.
Q5: How would you handle a scenario where a handler needs to "undo" its processing if a downstream handler fails?
Hints:
- Think about the Saga pattern or compensating transactions.
- Consider wrapping the chain in a two-phase approach: forward pass (process) and backward pass (rollback on failure), similar to how servlet filters have pre/post processing around
doFilter.
Counter Questions to Ask Interviewer
-
"Should exactly one handler process the request, or can multiple handlers act on it?" - Determines pure vs impure chain, which fundamentally changes the design.
-
"Is the chain static (configured at startup) or dynamic (modified at runtime)?" - Affects whether you need immutable snapshots, thread safety, or a chain builder.
-
"What happens if no handler can process the request?" - Clarifies whether you need a fallback handler, exception, or silent drop.
-
"Are there performance/latency constraints on the processing pipeline?" - Determines whether a chain is appropriate or if indexed dispatch is better.
-
"Do handlers need access to the results of previous handlers?" - Influences whether you pass a mutable context object through the chain or use return values.
References & Whitepapers
-
Gamma, E., Helm, R., Johnson, R., Vlissides, J. - Design Patterns: Elements of Reusable Object-Oriented Software (GoF, 1994), Chapter 5: Chain of Responsibility, pp. 223-232.
-
Java Servlet Specification (Jakarta EE) -
javax.servlet.FilterChaininterface. Defines the standard filter chain mechanism for web applications. Jakarta Servlet 6.0 Specification. -
Express.js Middleware Documentation - Describes the
next()function pattern for middleware chaining. expressjs.com/en/guide/using-middleware. -
Django Middleware Documentation - Layered middleware processing with
process_request/process_responsehooks. docs.djangoproject.com/en/stable/topics/http/middleware/. -
Fowler, M. - Patterns of Enterprise Application Architecture (2002). Discusses intercepting filters and pipeline patterns in enterprise contexts.
-
Grand, M. - Patterns in Java, Volume 1 (2002). Extended treatment of Chain of Responsibility with Java-specific idioms.
Related Topics
- Observer Pattern - One-to-many notification vs sequential chain
- Command Pattern - Encapsulating requests as objects passed through chains
- Decorator Pattern - Always-forwarding wrappers vs conditional forwarding
- Strategy Pattern - Selecting one algorithm vs chaining multiple potential handlers
- Mediator Pattern - Centralised communication vs distributed chain
- Template Method Pattern - Fixed algorithm skeleton vs flexible handler ordering
- Composite Pattern - Tree traversal vs linear chain traversal