If you remember five things
The whole pattern in one glance. Everything below is the defense of these five calls.
- One question per link. Each handler answers "is this mine?" and nothing else. If yes, handle it. If no, hand it to the next one.
- Base class owns the forwarding. The abstract handler is the only place that decides stop-or-forward, so a concrete handler can never forget to call next.
- The client builds the chain, not the links. Handlers do not know who comes after them by name. The sender holds only the head.
- Pick your stopping rule out loud. Pure chain (one owner, then stop) versus pipeline (everyone runs). Say which you are building before you code.
- Always design the tail. Decide what happens when a request falls off the end: throw, log, or a catch-all. A silent drop is the classic bug.
1The problem it solves#
Start with the pain, because the pattern only makes sense once you feel it. Say you are building an expense approval system. An employee submits a reimbursement, and the rule is tiered:
- Up to 1000, a team lead approves it.
- Up to 5000, a manager approves it.
- Up to 20000, a director approves it.
- Anything higher goes to the CFO.
The naive version looks like this:
if (amount <= 1000) {
teamLead.approve(request);
} else if (amount <= 5000) {
manager.approve(request);
} else if (amount <= 20000) {
director.approve(request);
} else {
cfo.approve(request);
}
It works. Until it does not. Watch what every real change does to that block:
- Add a VP tier at 50000: crack open the method, get the ordering right, retest everything.
- Thresholds vary by department (finance caps the manager at 3000, engineering at 8000): the one block now needs context it did not have.
- CFO is on leave, so requests skip to the board: more branching in the same spot.
The real problem is two coupled problems.
- The sender is bound to the exact handler that will process the request, so it cannot stay stable while approvers come and go.
- The branching logic lives in one brittle block that grows linearly with every new rule, and that block is the thing you keep breaking.
The caller also has to know every approver object and every limit by heart. That is a lot of knowledge living in exactly the wrong place: the sender.
This pattern exists to move the "who handles it" decision out of the caller and into the handlers themselves. The caller should drop the request at the front of a line and stop caring who owns it.
2The idea (mental model)#
Reach for a picture you already know. Think of a customer support line:
- You call in and talk to tier-1 support. If they can fix it, done.
- If not, they escalate to tier-2. Tier-2 either handles it or kicks it up to engineering.
- You, the caller, never pick who solves your problem. You drop it at the front and it flows until someone owns it.
That is Chain of Responsibility. You build a linked list of handlers, and each link makes exactly one decision:
- Can I handle this myself? If yes, handle it.
- If no, pass it to the next handler.
- The sender holds a reference to the first handler and nothing else.
Two principles fall out of this for free:
- Single Responsibility at the object level: each link owns one rule.
- Open/Closed at the system level: you extend the chain by adding a link, not by editing the ones already there.
Two flavors show up in the wild, and the only difference is the stopping rule:
- Pure chain: exactly one handler processes the request and the chain stops. The expense approval above is this flavor.
- Pipeline style: every handler does a little work and keeps passing it along, like HTTP middleware. Nobody short-circuits unless something goes wrong.
Both flavors are fair game, and I am listening for whether you name which one you are building before you start coding. The stopping rule is the exact thing I will probe, so get ahead of me and say it out loud: "this is a pure chain, so the first owner stops it."
3Structure#
Keep the skeleton small, because it is the same every time. The core is one abstract handler:
- It holds a reference to the
nexthandler and defines thehandlemethod. - Concrete handlers extend it, each implementing its own slice of logic.
- The deciding is split: the concrete class answers "is this mine?" and the base class owns "if not, forward it."
- The client builds the chain by linking handlers, then fires the request at the head. Nobody in the middle knows the full topology; each link only knows its successor.
Approver o-- Approver: a handler aggregates the next handler. Draw the inheritance arrows but forget that self-aggregation and you have accidentally drawn Strategy. Point at the o-- edge and name it when you present.Now the runtime flow when a 4000 request comes in. Team lead caps at 1000, so it forwards once and the manager owns it:
4Implementation in Java#
Here is the clean version of the expense flow. The thing to watch: the base class owns the "pass it on" logic, so the concrete handlers stay tiny, just a rule and a label each.
class ExpenseRequest {
final double amount;
final String purpose;
ExpenseRequest(double amount, String purpose) {
this.amount = amount;
this.purpose = purpose;
}
}
abstract class Approver {
protected Approver next;
// returns the argument so we can chain setNext calls fluently
Approver setNext(Approver next) {
this.next = next;
return next;
}
void approve(ExpenseRequest req) {
if (canApprove(req)) {
System.out.println(label() + " approved " + req.purpose
+ " for " + req.amount);
} else if (next != null) {
next.approve(req);
} else {
System.out.println("No one could approve " + req.purpose);
}
}
protected abstract boolean canApprove(ExpenseRequest req);
protected abstract String label();
}
class TeamLead extends Approver {
protected boolean canApprove(ExpenseRequest req) { return req.amount <= 1000; }
protected String label() { return "Team Lead"; }
}
class Manager extends Approver {
protected boolean canApprove(ExpenseRequest req) { return req.amount <= 5000; }
protected String label() { return "Manager"; }
}
class Director extends Approver {
protected boolean canApprove(ExpenseRequest req) { return req.amount <= 20000; }
protected String label() { return "Director"; }
}
Wiring it up stays readable because setNext returns the next link, so the chain reads top to bottom in the order requests flow:
public class Demo {
public static void main(String[] args) {
Approver lead = new TeamLead();
lead.setNext(new Manager()).setNext(new Director());
lead.approve(new ExpenseRequest(900, "Keyboard")); // Team Lead
lead.approve(new ExpenseRequest(4000, "Monitor")); // Manager
lead.approve(new ExpenseRequest(18000, "Laptop")); // Director
lead.approve(new ExpenseRequest(90000, "Server")); // falls off the end
}
}
Three things I want you to notice in this code:
- The client only touches
lead, the head of the chain, so the sender stays decoupled from every other approver. - Adding a VP tier at 50000 means one new class and one
setNext, with zero edits to existing handlers. That is the Open/Closed Principle paying off. - The base
approveis the only place that decides stop-or-forward, so a concrete handler never has to remember to callnext. The one line everyone forgets to write only exists once, which kills a whole class of bugs.
Want the pipeline flavor instead? Drop the early return:
- Let every handler do its bit and always call
next.approve(req)at the end. - The tail is a no-op when
nextis null. - That is exactly how request middleware works, and it is a two-line change from the code above.
Reveal the answer
5A real-world example#
You have already used this pattern a hundred times whether you knew it or not. Here is where it hides:
- Servlet filters and web middleware. Every
javax.servlet.Filteror Express middleware is a handler: auth, logging, rate limiting, CORS. The request walks the chain, and any link can short-circuit. An auth filter returning 401 stops the walk before the request reaches your controller. This is the pipeline flavor with a conditional short-circuit. - Logging frameworks. A log event flows through handlers filtered by level. A DEBUG message might hit the console handler and skip the file handler, while an ERROR hits both plus a pager. Each handler decides independently, which is why you can reconfigure logging without recompiling.
- Event handling in UI toolkits. A click lands on a button. If the button does not consume it, the event bubbles up to the parent panel, then the window. That bubbling is a chain, and "consume the event" is the stop signal.
- Try-catch blocks. Honestly the language-level version of this. An exception propagates up the call stack until a handler that can deal with that type catches it, and an uncaught exception at the top is your "fell off the end" case.
In an LLD setting, the classic prompts are:
- An ATM dispensing cash, where each denomination handler peels off what it can and passes the remainder down to smaller notes.
- A logging library.
- The expense or leave-approval workflow above.
If an interviewer describes anything with "escalation" or "tiers" or "falls through to," your antenna should go up before they finish the sentence.
6When to use it, and when not to#
Pattern discipline is knowing when not to reach for the thing. Reach for it when:
- Multiple objects might handle a request and you do not know which one until runtime.
- You want to add, remove, or reorder handlers without touching the others.
- You find yourself writing a long if-else or switch that keeps growing with new cases.
- Handlers form a natural order (cheapest check first, most expensive last) so you fail fast.
Skip it when:
- Exactly one handler always deals with every request. A map lookup keyed on type is clearer and O(1) instead of O(n).
- The order never changes and there are only two cases. A chain is overkill; an if-else is more honest about the intent.
- A request must be handled and you cannot tolerate it silently falling off the end. The chain does not promise anyone will catch it, so you have to design a default tail handler on purpose and say so.
The pattern earns its keep when the set of handlers is likely to change. Freeze the tiers forever and you are adding indirection for nothing.
If the tiers are frozen and will be two cases for the life of the system, a senior interviewer will read the chain as over-engineering. Name that trade-off yourself before they do.
7Common mistakes#
These are the ones I watch for, and most of them are the difference between a pass and a near-miss:
- Forgetting the default case. A request reaches the end and nothing handles it. Swallow that silently and you get a bug that is maddening to trace: no exception, no log, just a request that vanished. Always decide what the tail does: throw, log, or route to a catch-all you add on purpose.
- Putting the forwarding logic in every concrete handler. If each subclass writes
if (!canHandle) next.handle(req), one of them will forget and become a silent dead end. Push it into the base class so handlers only express their own rule. This is why the baseapproveowns the branching in the code above. - Building the chain inside the handlers. Handlers should not know who comes next by name. The client, or a dedicated builder, assembles the order. Keep that knowledge out of the links, otherwise reordering means editing handler code and you have lost the Open/Closed win.
- Cycles. Link A to B to A and a request loops forever. If your chain is built dynamically from config, guard against revisiting with a visited set or by validating the topology at build time.
- Handlers depending on each other's internals. Each link should be usable and testable on its own. If the Manager handler reaches into the Director's state, you have coupled the two classes you were trying to separate.
- Too-long chains on a hot path. Every link is a method call and a decision. For a latency-sensitive path with twenty handlers, measure before you ship. "I would check the p99 before worrying" is the answer that sounds senior.
Writing if (!canHandle) next.handle(req) inside every concrete handler, then letting one of them silently drop the request. If you cannot say what happens when a request falls off the end of the chain, I do not trust you to run this in production. The tail is not an afterthought; it is a design decision you make on purpose.
8Where it shows up in interviews#
Expect it in two forms:
- Named directly: "implement a logging framework" or "design an approval workflow."
- Disguised, where the signal is language: "escalate," "fall through," "try each in order," "the first one that can," or "a pipeline of checks" all point here.
When you spot it, present in this order:
- Say the name.
- Sketch the abstract handler with a
nextreference and thehandlemethod on the board, before writing a single concrete class. - Then write the concretes.
Interviewers want to see three things, in this order:
- You put the forwarding logic in the base class.
- The client builds the chain, the links do not.
- You have an answer for the unhandled request.
One last tip worth saying unprompted. If they push on flexibility, mention you can store handlers in a List<Handler> and iterate instead of a linked next. That makes reordering and config-driven setup trivial because the order lives in data, not in code, which is friendlier to systems where ops wants to toggle a filter without a deploy.
"How would you add a new handler without changing existing code?"
"Pure chain or pipeline, and how do you choose?"
"What happens when a request reaches the end and nobody handled it?"
"Why not just a switch statement? Isn't that simpler?"
"The chain has twenty links on a hot request path. Concern?"
What you can now explain in the room
- Why a growing if-else ladder couples the sender to every handler, and how the chain breaks that coupling.
- Why the forwarding logic belongs in the base class and never in the concrete handlers.
- The difference between a pure chain and a pipeline, and how to switch between them in one line.
- Why the client builds the chain while the links stay ignorant of each other.
- What the tail of the chain must do, and why a silent drop is the classic bug.
- When a switch or a map lookup is the better call, so the pattern does not read as over-engineering.
If you liked this, look at the Decorator pattern next (same wrapping instinct, different goal: Decorator adds behavior while every wrapper runs, a chain picks one owner and stops) and try the ATM cash-dispenser and logging-framework LLD problems to practice building chains under time pressure.
Get a prep plan built around your Chain of Responsibility Pattern interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.