Chain of Responsibility Pattern
Pass a request along a chain of handlers until one handles it. The foundation of middleware pipelines, servlet filters, event bubbling, and approval workflows.
Why Chain of Responsibility Exists
You are building a support ticket system. Incoming tickets have different severity levels: Basic questions go to a FAQ bot, Standard issues go to a support agent, Urgent bugs go to a senior engineer, and Critical outages go to the VP of Engineering. The naive approach: the ticket router uses a giant if/else chain that hard-codes every handler.
What goes wrong:
- Giant conditional — the router knows every handler type and every routing rule; adding a new severity level or handler means editing this monolith
- Tight coupling —
TicketRouterdepends onFaqBot,SupportAgent,SeniorEngineer, andVpEngineeringdirectly; it must import and instantiate all of them - No dynamic configuration — you can’t add, remove, or reorder handlers at runtime; the chain is hard-wired at compile time
- Single Responsibility violation — the router does both deciding who handles and knowing what each handler does; these are separate concerns
- No fallback logic — if a handler can’t process a ticket (e.g. agent is offline), there’s no automatic escalation to the next handler
This is the problem Chain of Responsibility solves — link handlers into a chain where each handler either processes the request or passes it to the next handler. The sender doesn’t know which handler will process it. Handlers can be added, removed, or reordered at runtime without modifying any existing code.
What Is Chain of Responsibility?
Chain of Responsibility is a behavioral pattern that lets you pass a request along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain. The sender of the request doesn’t know which handler will ultimately process it — it only knows the first link in the chain. This decouples senders from receivers and lets you compose processing pipelines dynamically.
— Gamma, Helm, Johnson, Vlissides (1994)
Key insight: Chain of Responsibility transforms a rigid conditional (if A else if B else if C) into a linked list of objects. Each object has a single responsibility: check if it can handle the request, and if not, delegate to the next. This makes the routing logic distributed (each handler owns its own rules), composable (assemble chains dynamically), and extensible (add a handler = add a link). Two variants exist: classic (exactly one handler processes the request โ the first that can) and pipeline (every handler processes the request and passes it to the next โ like servlet filters or middleware).
next reference pointing to the next handlerhandle(request)Participants & Structure
| Participant | Role | In the Analogy |
|---|---|---|
| Handler (interface / abstract) | Declares the handle(request) method and an optional setNext(handler) method. Typically an abstract class that implements the chaining logic so concrete handlers only override the processing logic. | The “approver” contract — every approver can approve or escalate. |
| Base Handler (abstract class) | Implements the default chaining boilerplate: stores the next reference, provides setNext(), and delegates to next.handle() if the current handler doesn’t process the request. Concrete handlers extend this. | The shared escalation procedure — “if I can’t approve, pass it up.” |
| Concrete Handler | Overrides handle() with its own processing logic. Decides whether to process the request or pass it to next. Each handler is independent and self-contained. | Team Lead (โค$1K), Manager (โค$5K), Director (โค$50K), CFO (unlimited). |
| Client | Assembles the chain (links handlers together) and sends the request to the first handler. The client doesn’t know which handler will process it. | The employee submitting the expense report — just hands it to their team lead. |
- In the classic variant, the first handler that can process the request does so and stops the chain (e.g. expense approval).
- In the pipeline variant, every handler processes the request and passes it along (e.g. servlet filters, HTTP middleware).
- Both use the same structure — the difference is whether
handle()callsnext.handle()after processing or instead of processing.
The Pattern In Motion
Scenario: An expense approval chain. An employee submits a $8,000 expense. The chain is: Team Lead (โค$1K) โ Manager (โค$5K) โ Director (โค$50K) โ CFO (unlimited).
lead.setNext(manager).setNext(director).setNext(cfo)lead (the first link). It doesn’t know about manager, director, or CFO.lead.handle(new Expense("Travel", 8000))TeamLeadHandler checks: $8,000 > $1,000 limit. Can’t approve. Passes to next.handle(expense).ManagerHandler receives the requestnext.handle(expense).DirectorHandler receives the request- The UML (S03) shows who participates. The flow diagram above shows how a request traverses the chain.
- For Chain of Responsibility, the key runtime insight is: the request enters at one end and exits when a handler processes it (classic) or after every handler has touched it (pipeline).
- The client never knows which handler acted.
You Already Use This
Chain of Responsibility is the pattern behind every middleware pipeline and filter stack in Java. Whenever you stack filters that each decide to process or pass along, you’re using this pattern.
javax.servlet.Filter / jakarta.servlet.Filter — the definitive Chain of Responsibility in Java web apps. Each filter implements doFilter(request, response, chain). It can inspect/modify the request, then call chain.doFilter() to pass it to the next filter. Filters are chained in web.xml or via @WebFilter annotations. Authentication โ Logging โ Compression โ Servlet — a classic pipeline chain. java.util.logging.Logger — loggers form a parent-child chain. When you call logger.log(), the logger checks its level; if it can’t handle the message (level too low), it passes the LogRecord to its parent logger. The root logger is the final handler. This is the classic variant: the first logger that accepts the level handles it. java.awt.event — AWT event handling uses chain of responsibility via component hierarchy. When a mouse click occurs on a button, the button’s handler gets first chance. If unhandled, it propagates to the parent panel, then to the frame. This is “event bubbling” — Chain of Responsibility along the component tree. Spring HandlerInterceptor — Spring MVC registers interceptors that form a chain. preHandle() runs before the controller; if it returns false, the chain stops (classic variant). postHandle() runs after — pipeline variant. Spring Security’s FilterChainProxy is an explicit filter chain. - Unlike classic CoR where the first capable handler stops the chain, servlet filters use the pipeline variant: every filter processes the request (adding headers, checking auth, compressing response) and then calls
chain.doFilter()to pass it along. - The request flows through the entire chain.
- Both variants use the same structural pattern — the difference is whether handlers call
nextafter processing (pipeline) or instead of processing (classic).
Build It Once
Domain: Support Ticket Escalation. A BaseHandler abstract class manages the chain link. Concrete handlers (FaqBotHandler, SupportAgentHandler, EngineerHandler) each handle tickets within their capability or escalate to the next handler.
- Notice
setNext()returns the next handler, notthis. - This lets you chain:
a.setNext(b).setNext(c). - Some implementations return
thisinstead — both work, but returning the next handler makes assembly more natural for linear chains.
Common Mistakes
passToNext() print a warning when next == null (as shown in S06).
- If
FaqBotHandlercreates or referencesSupportAgentHandlerinternally, you’ve destroyed the decoupling benefit. - Each handler should know only the
Handlerinterface and itsnextreference. - Fix: Chain assembly happens in the client (setup code), never inside handlers. Handlers call
passToNext()blindly.
next.handle() in pipeline variant: - In a middleware/pipeline chain, every handler must call
next.handle()after processing โ otherwise the pipeline breaks silently. - An auth filter that rejects but forgets to skip
nextproperly will let unauthenticated requests through. - Fix: In pipeline variant, always call
next.handle()unless you explicitly want to stop the chain (e.g. auth rejection).
- If handler A’s
nextpoints to B, and B’snextpoints back to A, you get infinite recursion and aStackOverflowError. - Fix: Assemble chains linearly. If you build chains dynamically (from config), validate that no cycles exist before activating.
Map<Type, Handler> suffices: - If every request type maps to exactly one handler and the mapping never changes, a
Maplookup is O(1) vs. - CoR’s O(n) traversal.
- Fix: Use CoR only when: the handler isn’t known at compile time, handlers need to inspect the request to decide, or multiple handlers may process in sequence (pipeline).
When To Use Chain of Responsibility
- Multiple objects may handle a request, and the handler isn’t known at compile time — it’s determined by inspecting the request at runtime
- You want to decouple the sender from all potential receivers — the client sends to the chain, not to a specific handler
- You need to dynamically configure which handlers run and in what order — add, remove, or reorder without changing sender code
- You are building a middleware/filter pipeline — every handler processes the request and passes it along (logging โ auth โ rate-limit โ handler)
- You want to follow Open/Closed — adding a new handler type means adding a class, not editing a router
- The mapping from request type to handler is fixed and known — a
Map<Type, Handler>orswitchis simpler and faster - Exactly one handler must always process every request — if there’s no “passing along,” there’s no chain
- You need guaranteed handling — CoR allows requests to fall off the end unhandled (unless you add a catch-all)
- Order of handlers doesn’t matter and each is independent — use Observer (broadcast) instead of CoR (sequential)
| Pattern | Request Flow | How Many Handle? | When to pick it |
|---|---|---|---|
| Chain of Resp. ← this | Sequential (linked list) | One (classic) or all (pipeline) | Request needs to find the right handler at runtime |
| Observer | Broadcast (fan-out) | All observers notified | One event, many independent reactions |
| Command | Direct (invoker โ command) | Exactly one command | Encapsulate a request for undo/queue/log |
| Decorator | Wrapping (nested calls) | All decorators + core | Add behaviour to an object, not route requests |
| Mediator | Hub (central coordinator) | Mediator decides | Complex many-to-many communication |
Problems To Solve
Chain of Responsibility problems test whether you can decouple request senders from receivers, build composable handler chains, and handle edge cases like unhandled requests and chain reconfiguration.
| Difficulty | Problem | Key Insight |
|---|---|---|
| Easy | ATM Cash Dispenser Build a chain of bill dispensers: $100 โ $50 โ $20 โ $10. When a withdrawal is requested (e.g. $280), the $100 handler dispenses 2 bills ($200), passes the remaining $80 to the $50 handler (1 bill, $30 remaining), then $20 handler (1 bill, $10 remaining), then $10 handler (1 bill). Each handler reduces the amount and passes the rest to the next. | Tests the pipeline variant: every handler processes and passes along. Each handler uses integer division (amount / denomination) to determine bill count, then passes amount % denomination to next. The chain order matters — largest bills first. |
| Medium | HTTP Middleware Pipeline Build a middleware chain for an HTTP server: LoggingMiddleware โ AuthMiddleware โ RateLimitMiddleware โ RouteHandler. Each middleware can modify the request, pass it to next, or short-circuit (e.g. auth failure returns 401). Implement both preProcess() and postProcess() so middleware can act on both the request and response. | Tests the pipeline variant with bidirectional processing. The key insight: middleware must call next.handle() between pre and post processing — like servlet filters. If auth fails, it short-circuits (returns 401 without calling next). If it passes, it calls next, then runs post-processing on the response (e.g. logging response time). |
| Medium | Dynamic Help System Build a UI help system where clicking “Help” on a component traverses the component tree upward: Button โ Panel โ Dialog โ App. Each component may or may not have help text. The first component with help text displays it. If none has help, show “No help available.” Allow components to add/remove help text at runtime. | Tests the classic variant with a tree-based chain (not a flat list). Each component’s next is its parent in the UI tree. The traversal follows the component hierarchy โ this is exactly how event bubbling works in DOM and AWT. The dynamic aspect: help text can change at runtime without rebuilding the chain. |
| Hard | Rule Engine with Priority and Short-Circuit Build a rule engine for an e-commerce discount system. Rules: “VIP gets 20%”, “Order > $500 gets 10%”, “First-time buyer gets 15%”, “Max one discount per order.” Rules are prioritized. The chain should: (1) apply at most one discount (classic variant), (2) support dynamic reordering by priority, (3) allow new rules to be plugged in via config, (4) log which rule fired and which were skipped. | Tests advanced CoR with dynamic ordering and observability. Handlers are sorted by priority before chain assembly. Each handler checks its condition; the first match fires and stops the chain. Logging middleware wraps each handler to record decisions. Dynamic reordering means the chain is rebuilt when priorities change — test that the client code doesn’t change. |
- When asked about Chain of Responsibility, the interviewer wants to see: (1) a
Handlerinterface withhandle()andsetNext(); (2) a base class that manages thenextreference and chaining logic; (3) concrete handlers that either process or delegate; (4) a client that assembles the chain without knowing which handler will act. - Stand-out answers mention: the two variants (classic vs. pipeline),
javax.servlet.Filteras the canonical JDK example, how it differs from Decorator (CoR can stop the chain; Decorator always wraps), and the tradeoff — CoR has O(n) traversal cost and requests can go unhandled if there’s no catch-all.