Pass a request along a chain of handlers — each handler decides to process it or pass it to the next.
IntermediatePattern #13 of 23
01 — Intent
What is Chain of Responsibility?
Chain of Responsibility lets you pass a request through a chain of handler objects. Each handler decides either to process the request and stop the chain, or pass it to the next handler. The sender doesn't know which handler will ultimately process it.
This decouples the sender from all potential receivers — you can build, reorder, or extend the chain without changing any existing handler or the sender.
Real-world analogy: A customer support escalation chain — Level 1 support handles basic issues. If it can't resolve it, passes to Level 2. If still unresolved, escalates to Level 3. Each level either handles or passes on.
02 — Problem
The Problem It Solves
You're building an expense approval system. Requests under $500 are approved by a Team Lead. $500–$5000 by a Manager. $5000–$50000 by a Director. Above that, only the CEO can approve. Hardcoding a long if-else chain couples the logic in one place and makes it painful to add new approval levels.
Chain of Responsibility lets each approver be a self-contained handler. Assembling or reordering the chain at runtime requires zero changes to handler code.
03 — Solution
Participants
Handler (interface)
Declares the interface for handling requests. Usually contains a method to set the next handler and a method to handle the request.
BaseHandler (abstract)
Optional boilerplate class implementing the chain-linking logic. Stores next handler reference and calls it if the current handler passes the request.
ConcreteHandler
Handles requests it is responsible for. Calls next handler for everything else. Self-contained — knows only its own logic and the next handler.
Client
Assembles the chain and sends requests to the first handler. Unaware of which specific handler processes each request.
04 — Structure
Visual Flow Diagram
05 — Implementation
Java Code Example
Java — Expense Approval Chain
// Handler interfacepublic interfaceApprover {
voidsetNext(Approver next);
voidapprove(double amount);
}
// Abstract base handler — boilerplate chain logicpublic abstract classBaseApproverimplementsApprover {
privateApprover next;
publicApproversetNext(Approver next) {
this.next = next;
return next; // allows chaining: a.setNext(b).setNext(c)
}
protected voidpassToNext(double amount) {
if (next != null) {
next.approve(amount);
} else {
System.out.println("No handler available for $" + amount);
}
}
}
// Concrete Handlerspublic classTeamLeadextendsBaseApprover {
public voidapprove(double amount) {
if (amount < 500) {
System.out.println("TeamLead approved $" + amount);
} else {
passToNext(amount); // can't handle — pass up
}
}
}
public classManagerextendsBaseApprover {
public voidapprove(double amount) {
if (amount < 5_000) {
System.out.println("Manager approved $" + amount);
} else {
passToNext(amount);
}
}
}
public classDirectorextendsBaseApprover {
public voidapprove(double amount) {
if (amount < 50_000) {
System.out.println("Director approved $" + amount);
} else {
passToNext(amount);
}
}
}
// Client — assembles chainpublic classMain {
public static voidmain(String[] args) {
BaseApprover lead = newTeamLead();
BaseApprover manager = newManager();
BaseApprover director= newDirector();
// Build chain: lead → manager → director
lead.setNext(manager).setNext(director);
lead.approve(200); // TeamLead approved
lead.approve(1500); // Manager approved
lead.approve(20000); // Director approved
lead.approve(100000); // No handler available
}
}
06 — Applicability
When to Use / Avoid
✓ Use When
More than one object may handle a request, and the handler isn't known a priori
You want to issue a request to one of several objects without specifying the receiver explicitly
The set of handlers should be specifiable dynamically
Every request must be guaranteed to be handled — broken chains silently drop requests
Performance is critical — long chains add latency
The handler logic is simple and static — an if-else or strategy is simpler
07 — Real World
Real-World Examples
Java Servlet Filter chain — each filter processes the request and calls chain.doFilter()
Spring Security filter chain — authentication, authorization, CSRF filters in sequence
java.util.logging — Logger passes log records up the parent handler chain
Spring MVC HandlerInterceptor — preHandle chain for request interception
UI event bubbling in browsers — click event travels up from child to parent DOM nodes
08 — Trade-offs
Pros & Cons
Pros
Decouples sender from receivers — sender knows nothing about handlers
Single Responsibility — each handler does one thing
Open/Closed — add new handlers without changing existing ones
Chain order is configurable at runtime
Cons
No guarantee a request gets handled — can fall off the end of the chain
Hard to debug — difficult to trace which handler processed a request
Long chains hurt performance and stack depth
09 — Break & Prevent
How Chain of Responsibility Can Be Broken
⚠ Attack Vectors
Broken chain — request silently dropped: A handler neither processes the request nor calls passToNext() — the request disappears with no error
Circular chain: Handler A sets next to B, B sets next to A — infinite loop, StackOverflowError
Handler modifying shared request object: One handler mutates the request before passing it — downstream handlers see corrupted state
Assembling chain in wrong order: Chain is wired lead → director → manager instead of lead → manager → director — director approves small amounts that should go to manager
Stateful handlers shared across threads: Handler stores per-request state in instance fields — concurrent requests overwrite each other's state
✓ Prevention
Default passToNext in base class: The abstract base handler always calls next if defined — ConcreteHandlers only need to call super.handle() or passToNext() to stay in chain
Terminal handler: Always add a terminal catch-all handler at the end of the chain that logs or throws an exception if no handler could process the request
Immutable request objects: Make the request a value object with no setters — handlers can read it but never mutate it
Cycle detection: When building the chain with setNext(), check that the new next handler isn't already in the chain
Stateless handlers: Keep all handler classes stateless — all per-request data lives in the request object itself, not in handler fields
Java — Break & Fix
// ❌ BREAKING — handler swallows request silentlypublic voidapprove(double amount) {
if (amount < 500) {
System.out.println("Approved");
}
// ❌ else: nothing happens — request silently dropped
}
// ✅ FIX — always call passToNext in else branchpublic voidapprove(double amount) {
if (amount < 500) {
System.out.println("TeamLead approved");
} else {
passToNext(amount); // ✅ always pass if not handled
}
}
// ✅ Terminal handler — catches unhandled requestspublic classUnhandledApproverextendsBaseApprover {
public voidapprove(double amount) {
throw newIllegalStateException(
"No approver for amount: $" + amount); // fail loudly
}
}
// Wire it: lead.setNext(manager).setNext(director).setNext(new UnhandledApprover())// ❌ BREAKING — handler mutates shared requestpublic voidapprove(Request req) {
req.setAmount(req.getAmount() * 0.9); // ❌ mutates — next handler sees 10% lesspassToNext(req);
}
// ✅ FIX — immutable request objectpublic recordRequest(double amount, String description) {}
// records are immutable by default — no setters possible
10 — Related Patterns
How Other Patterns Relate
Command
Often Used Together
Command encapsulates requests as objects. Chain of Responsibility passes those Command objects along a chain of handlers. Together they form powerful pipeline architectures.
Decorator
Often Confused
Both chain objects together. Decorator always forwards to the next and adds behaviour. Chain of Responsibility may stop the chain at any point — only one handler typically processes a request.
Composite
Often Used Together
Chain of Responsibility is often used with Composite — a component's parent is its next handler. An event not handled by a leaf propagates up to its parent composite automatically.
11 — Quick Recap
Interview Cheat Sheet
What: A linked list of handlers. Request enters the first handler — it either processes it and stops, or passes to the next. Sender never knows which handler actually handles it.
How: Handler interface with setNext() and handle(). Abstract BaseHandler stores next reference and calls it. ConcreteHandlers check condition — handle or call passToNext(). Client wires chain and sends to first.
Critical interview point: Always add a terminal handler — silent request drops are the most common production bug with this pattern. Request objects should be immutable to prevent handler corruption.