Behavioral

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.

Overview ยท Behavioral ยท Singleton ยท Factory Method ยท Abstract Factory ยท Builder ยท Prototype ยท Adapter ยท Bridge ยท Composite ยท Decorator ยท Facade ยท Flyweight ยท Proxy ยท Observer ยท Strategy ยท Command ยท Template Method ยท Chain of Resp. ยท State ยท Mediator ยท Iterator ยท Visitor ยท Memento ยท Interpreter
Category: Behavioral Difficulty: Intermediate Interview: Tier 3 Confused with: Decorator
01
Section One ยท The Problem

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.

Naive approach — giant conditional routing
// โœ— Ticket router hard-codes every handler and every rule class TicketRouter { private FaqBot bot; private SupportAgent agent; private SeniorEngineer senior; private VpEngineering vp; public void route(Ticket ticket) { if (ticket.severity == Severity.BASIC) { bot.handle(ticket); } else if (ticket.severity == Severity.STANDARD) { agent.handle(ticket); } else if (ticket.severity == Severity.URGENT) { senior.handle(ticket); } else if (ticket.severity == Severity.CRITICAL) { vp.handle(ticket); } // Add a new level? Edit this class. Change order? Edit this class. } }

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 couplingTicketRouter depends on FaqBot, SupportAgent, SeniorEngineer, and VpEngineering directly; 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
Without Chain of Responsibility — central router hard-wired to every handler
TicketRouter FaqBot SupportAgent SeniorEngineer VpEngineering ✗ Router knows every handler by name ✗ Giant if/else decides routing ✗ Can't add/remove handlers at runtime ✗ No automatic escalation on failure ✗ New handler = edit router + add branch

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.

02
Section Two ยท The Pattern

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.

GoF 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.”
— Gamma, Helm, Johnson, Vlissides (1994)
Analogy — corporate expense approval: You submit an expense report for $5,000. It first goes to your team lead — they can approve up to $1,000, so they pass it along. It reaches the department manager — they can approve up to $5,000, so they approve it. If the expense were $50,000, the manager would pass it to the director, who would pass it to the CFO. You (the sender) don’t know who will ultimately approve your expense — you just submit it to the chain. Each approver either handles it (within their authority) or escalates it (passes to the next). The chain can be reconfigured: add a VP between manager and director, remove the team lead for senior employees — the process adapts without changing the submitter’s code.

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).

Each handler has a next reference pointing to the next handler
Handlers form a linked list. The sender calls the first handler; each handler decides to process or delegate.
Handlers share a common interface: handle(request)
The sender depends only on the Handler interface — not on any specific handler. Loose coupling.
Each handler encapsulates its own “can I handle this?” logic
No central router. Each handler knows its own rules. Adding a new handler = adding a new class, not editing a conditional.
The chain is assembled at runtime — handlers can be added, removed, or reordered
Different configurations for different contexts: dev chain (log everything) vs. prod chain (authenticate โ†’ authorize โ†’ rate-limit โ†’ handle).
03
Section Three ยท Anatomy

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.
Chain of Responsibility — UML Class Diagram
«interface» Handler + setNext(Handler) : Handler + handle(request) : void next «abstract» BaseHandler - next : Handler + setNext(h) : Handler + handle(req) : delegates to next TeamLeadHandler + handle(req) ManagerHandler + handle(req) DirectorHandler + handle(req) CfoHandler + handle(req) Client + submit(req) ■ Blue = interface / abstract ■ Green = concrete handler ▷ solid = extends    --▷ dashed = implements Each handler holds a “next” reference โ€” forming the chain
Classic vs. Pipeline variant:
  • 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() calls next.handle() after processing or instead of processing.
04
Section Four ยท How It Works

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).

Step 1 — Client assembles the chain: lead.setNext(manager).setNext(director).setNext(cfo)
Four handlers are linked. The client holds a reference only to lead (the first link). It doesn’t know about manager, director, or CFO.
Step 2 — Client submits: lead.handle(new Expense("Travel", 8000))
TeamLeadHandler checks: $8,000 > $1,000 limit. Can’t approve. Passes to next.handle(expense).
Step 3ManagerHandler receives the request
$8,000 > $5,000 limit. Can’t approve. Passes to next.handle(expense).
Step 4DirectorHandler receives the request
$8,000 โ‰ค $50,000 limit. Approved! The director processes the request. Chain stops here — CFO is never called.
Step 5 — A $500 expense follows the same chain
$500 โ‰ค $1,000. Team Lead approves immediately. Manager, Director, CFO are never called. Same chain, different outcome.
Chain of Responsibility — Request Traversal ($8,000 expense)
Team Lead โ‰ค $1,000 Manager โ‰ค $5,000 Director โ‰ค $50,000 โœ“ CFO unlimited pass pass skip Expense: $8,000 โœ“ APPROVED by Director $500 โ†’ Team Lead approves immediately $100K โ†’ CFO approves (passes through all 3) Same chain, different requests โ†’ different handlers process. Client never knows which handler approved.
The pattern in pseudocode
// โ”€โ”€ Client assembles the chain โ”€โ”€ Handler chain = new TeamLeadHandler(1000); chain.setNext(new ManagerHandler(5000)) .setNext(new DirectorHandler(50000)) .setNext(new CfoHandler()); // โ”€โ”€ Submit requests โ”€โ”€ chain.handle(new Expense("Lunch", 500)); // Output: โœ… Team Lead approved "Lunch" ($500) chain.handle(new Expense("Travel", 8000)); // Output: โฉ Team Lead passed "Travel" ($8000) // โฉ Manager passed "Travel" ($8000) // โœ… Director approved "Travel" ($8000) chain.handle(new Expense("Acquisition", 100000)); // Output: โฉ Team Lead passed ... โฉ Manager passed ... โฉ Director passed ... // โœ… CFO approved "Acquisition" ($100000)
Behavioral patterns need both static UML and dynamic sequence:
  • 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.
05
Section Five ยท Java Stdlib

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.

IN JAVA
Example 1 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.
Example 2 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.
Example 3 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.
Example 4 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.
Stdlib usage — Servlet Filter chain
// Each filter processes and passes along โ€” pipeline variant public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { if (!isAuthenticated(req)) { res.sendError(401); // โ† handle: reject request, stop chain return; } chain.doFilter(req, res); // โ† pass to next filter in chain } }
Stdlib usage — Logger parent chain
// Logger chain: child โ†’ parent โ†’ root (classic variant) Logger root = Logger.getLogger(""); root.setLevel(Level.WARNING); Logger app = Logger.getLogger("com.myapp"); app.setLevel(Level.INFO); // app.info("msg") โ†’ app handles it (Level.INFO โ‰ฅ INFO) // app.fine("msg") โ†’ app passes to root โ†’ root ignores (FINE < WARNING)
Servlet filters prove the pipeline variant:
  • 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 next after processing (pipeline) or instead of processing (classic).
06
Section Six ยท Implementation

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.

Java — Chain of Responsibility Ticket System (core)
// โ”€โ”€ Handler interface โ”€โ”€ interface TicketHandler { TicketHandler setNext(TicketHandler next); void handle(Ticket ticket); } // โ”€โ”€ Base handler with chaining boilerplate โ”€โ”€ abstract class BaseTicketHandler implements TicketHandler { private TicketHandler next; public TicketHandler setNext(TicketHandler h) { this.next = h; return h; // fluent chaining } protected void passToNext(Ticket t) { if (next != null) next.handle(t); else System.out.println(" โš  No handler for: " + t); } }
Fluent chain assembly:
  • Notice setNext() returns the next handler, not this.
  • This lets you chain: a.setNext(b).setNext(c).
  • Some implementations return this instead — both work, but returning the next handler makes assembly more natural for linear chains.
07
Section Seven ยท Watch Out

Common Mistakes

Mistake #1 — Request falls off the chain with no handler: If no handler in the chain can process the request, it silently disappears. The client thinks it was handled; nobody acted. Fix: Always add a catch-all handler at the end of the chain that logs a warning or throws an exception. Or have passToNext() print a warning when next == null (as shown in S06).
✗ Wrong — silent drop
// โœ— If no handler matches, nothing happens โ€” silent failure protected void passToNext(Ticket t) { if (next != null) next.handle(t); // โ† else: request vanishes silently }
✔ Correct — warn on unhandled
// โœ“ Log when no handler processes the request protected void passToNext(Ticket t) { if (next != null) next.handle(t); else log.warn("Unhandled request: " + t); }
Mistake #2 — Handlers know about each other:
  • If FaqBotHandler creates or references SupportAgentHandler internally, you’ve destroyed the decoupling benefit.
  • Each handler should know only the Handler interface and its next reference.
  • Fix: Chain assembly happens in the client (setup code), never inside handlers. Handlers call passToNext() blindly.
Mistake #3 — Forgetting to call 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 next properly 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).
Mistake #4 — Circular chain:
  • If handler A’s next points to B, and B’s next points back to A, you get infinite recursion and a StackOverflowError.
  • Fix: Assemble chains linearly. If you build chains dynamically (from config), validate that no cycles exist before activating.
Mistake #5 — Using CoR when a simple Map<Type, Handler> suffices:
  • If every request type maps to exactly one handler and the mapping never changes, a Map lookup 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).
08
Section Eight ยท Decision Guide

When To Use Chain of Responsibility

Use Chain of Responsibility When
  • 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
Avoid Chain of Responsibility When
  • The mapping from request type to handler is fixed and known — a Map<Type, Handler> or switch is 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)
Chain of Responsibility vs. Confused Patterns
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
Decision Flowchart
Request with multiple potential handlers? No Direct call Yes Handlers must run in sequence? No Observer (broadcast to all) Yes Only one handler should process? No CoR (pipeline) (all handlers process) Yes CoR (classic)
09
Section Nine ยท Practice

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.
Interview Tip:
  • When asked about Chain of Responsibility, the interviewer wants to see: (1) a Handler interface with handle() and setNext(); (2) a base class that manages the next reference 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.Filter as 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.