Chain of Responsibility Pattern
The Chain of Responsibility pattern lets a request travel along a line of handler objects. The sender does not know which handler will deal with the request; it simply hands it to the first handler in the chain. Each handler decides one of two things: I can handle this, or I cannot, so I pass it to the next handler. The request flows down the chain until some handler processes it or the chain runs out.
This decouples the sender of a request from its receiver. Instead of one object containing a tangle of conditionals deciding who does what, responsibility is spread across small, single-purpose handlers that can be added, removed, or reordered without touching the others.
The problem it solves
Imagine a customer-support system where every query — technical, billing, or general — is processed by a single CustomerSupport class packed with branching logic. That one class grows a giant if/else if cascade, knows about every category, and must be edited every time a new kind of query appears. It is hard to test, hard to extend, and violates the Single Responsibility Principle.
Chain of Responsibility breaks that monolith into a sequence of focused handlers — TechnicalSupportHandler, BillingSupportHandler, GeneralSupportHandler — each owning exactly one category. A query enters the chain at the first handler; if that handler does not own the category, it forwards the query to its successor, and so on until the right handler processes it.
Real-world analogy
Walking into a restaurant is the same shape. The host seats you and handles seating requests; a waiter takes your order and handles service questions; the chef steps in for a custom dish; the manager handles a billing dispute. Each role handles what falls in its remit and escalates the rest. Your request moves through that line of people exactly as a request moves through a chain of handlers.
Structure
The pattern has three roles:
- Handler — an abstract class or interface declaring the request-handling method and holding a reference to the next handler in the chain.
- Concrete Handlers — subclasses that each handle one specific kind of request. When a concrete handler cannot deal with a request, it forwards it to its successor.
- Client — builds the chain (wiring each handler to the next) and sends requests to its head. The client can assemble or reorder the chain dynamically.
The client sends to the first concrete handler. If that handler can process the request it does so; otherwise it forwards to the next. This continues until the request is handled or the chain ends.
Implementation
The abstract SupportHandler holds the link to the next handler and declares handleRequest. Each concrete handler checks whether the query is its category; if so it processes it, otherwise it forwards to nextHandler. Note the forwarding line is identical in every handler — that duplication is deliberate here for clarity, but it is exactly the thing the pattern is most often gotten wrong on (see Pitfalls).
enum QueryType { TECHNICAL, BILLING, GENERAL }
abstract class SupportHandler {
protected SupportHandler nextHandler;
public void setNextHandler(SupportHandler next) {
this.nextHandler = next;
}
public abstract void handleRequest(QueryType type, String message);
}
class TechnicalSupportHandler extends SupportHandler {
@Override
public void handleRequest(QueryType type, String message) {
if (type == QueryType.TECHNICAL) {
System.out.println("Technical Support: " + message);
} else if (nextHandler != null) {
nextHandler.handleRequest(type, message);
}
}
}
class BillingSupportHandler extends SupportHandler {
@Override
public void handleRequest(QueryType type, String message) {
if (type == QueryType.BILLING) {
System.out.println("Billing Support: " + message);
} else if (nextHandler != null) {
nextHandler.handleRequest(type, message);
}
}
}
class GeneralSupportHandler extends SupportHandler {
@Override
public void handleRequest(QueryType type, String message) {
if (type == QueryType.GENERAL) {
System.out.println("General Support: " + message);
} else if (nextHandler != null) {
nextHandler.handleRequest(type, message);
}
}
}
// Client assembles the chain
SupportHandler tech = new TechnicalSupportHandler();
SupportHandler billing = new BillingSupportHandler();
SupportHandler general = new GeneralSupportHandler();
tech.setNextHandler(billing);
billing.setNextHandler(general);
tech.handleRequest(QueryType.BILLING, "Question about my invoice.");The fix the Pitfalls section points to is to hoist forwarding into the base class and add a terminal default so an unowned request is never silently dropped — a small final template that every handler inherits:
abstract class SupportHandler {
protected SupportHandler nextHandler;
public void setNextHandler(SupportHandler next) { this.nextHandler = next; }
// final: the walk-or-forward logic is written ONCE, not copy-pasted per handler
public final void handle(QueryType type, String message) {
if (canHandle(type)) { doHandle(message); return; }
if (nextHandler != null) { nextHandler.handle(type, message); return; }
throw new UnhandledRequestException(type); // terminal: never a silent drop
}
protected abstract boolean canHandle(QueryType type);
protected abstract void doHandle(String message);
}Now a concrete handler only declares what it owns (canHandle) and what it does (doHandle); the forwarding and the end-of-chain rejection are impossible to get wrong because no handler writes them.
The same design in Go uses a slice of small handlers or a linked successor field rather than inheritance:
type QueryType int
const (
Technical QueryType = iota
Billing
General
)
type Handler interface {
Handle(t QueryType, msg string)
SetNext(h Handler)
}
type base struct{ next Handler }
func (b *base) SetNext(h Handler) { b.next = h }
func (b *base) forward(t QueryType, msg string) {
if b.next != nil {
b.next.Handle(t, msg)
}
}
type TechnicalHandler struct{ base }
func (h *TechnicalHandler) Handle(t QueryType, msg string) {
if t == Technical {
fmt.Println("Technical Support:", msg)
return
}
h.forward(t, msg)
}
type BillingHandler struct{ base }
func (h *BillingHandler) Handle(t QueryType, msg string) {
if t == Billing {
fmt.Println("Billing Support:", msg)
return
}
h.forward(t, msg)
}
// Client
tech := &TechnicalHandler{}
billing := &BillingHandler{}
tech.SetNext(billing)
tech.Handle(Billing, "Question about my invoice.")Pulling the forward step into a shared base (Go) or a template method on SupportHandler (Java) removes the copy-pasted nextHandler != null line from every handler.
Variants
Two design choices vary independently of one another.
Pure vs. impure handling
In a pure chain exactly one handler processes a request and forwarding stops there. In an impure (pipeline) chain, a handler may do part of the work and still forward — a logging handler logs and passes on, an auth handler validates and passes on. Servlet filters and HTTP middleware are pipeline chains: every link runs.
Explicit vs. implicit successor
An explicit successor is a next field each handler holds and calls (shown above). An implicit chain stores handlers in a list and an external loop walks them, so handlers do not know about each other at all — easier to reorder, but the per-handler “stop or continue” decision now has to be signalled by a return value instead of by simply not forwarding.
Pitfalls
These are the failure modes to watch for — the forward-references above point here.
- Unhandled requests fall off the end silently. If no handler owns a request, the naive version just returns and nothing happens — no error, no log. Getting this wrong is the most common bug. Always provide a terminal default handler (or have the client detect “handled = false”) so an unowned request is explicitly rejected rather than quietly dropped.
- Forwarding duplication. The
if (nextHandler != null) nextHandler.handleRequest(...)line repeated in every handler is easy to copy wrong — forget it in one handler and the chain dead-ends there. Hoist forwarding into the base class or a template method, as shown in the Implementation section. - Order dependence is invisible. Behaviour depends on the order handlers are wired, but that order lives only in the client’s assembly code. Document it, and prefer building the chain in one place.
- Broken or cyclic links. A mis-wired successor (or an accidental cycle) causes silent drops or infinite recursion. Validate the chain when you build it.
When to use it — and when not to
This is the decision the pattern actually turns on, set against the two alternatives people reach for first.
Reach for Chain of Responsibility when
- Several handlers could process a request and you do not want the sender to know which one will.
- The set of handlers, or their order, should be configurable or changeable at runtime.
- You want ordered fall-through: try handler A, then B, then C — or a pipeline where several handlers each contribute (logging, auth, then business logic).
Prefer a switch / if-else instead when
The categories are fixed, few, and known at compile time, and there is no notion of escalation or fall-through. A single conditional is the least machinery and keeps all the logic visible in one place. Only graduate to a chain once that conditional starts attracting unrelated responsibilities or changing on every new case. Do not introduce a chain purely to avoid an if.
Prefer Strategy / a map lookup instead when
Each request maps to exactly one handler by a key, with no ordered trying and no fall-through. A Map<Key, Handler> gives O(1) direct dispatch and stays open for extension, whereas a chain walks O(n) links to reach the same handler. Choose the map when dispatch is a clean one-to-one lookup; choose the chain only when “ask each in turn until someone takes it” is genuinely the behaviour you need. If you find yourself wanting both keyed dispatch and ordered fallback, that is the signal a chain (not a map) is the right tool.
Source
Adapted and rewritten from the project lesson Chain of Responsibility Pattern (site/oo-low-level-design/behavioral/001-chain-of-responsibility-pattern.html), with the classic definition from Gamma, Helm, Johnson & Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software (1994).
🤖 Don't fully get this? Learn it with Claude
Stuck on Chain of Responsibility Pattern? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Chain of Responsibility Pattern** (OO & Low-Level Design) and want to truly understand it. Explain Chain of Responsibility Pattern from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Chain of Responsibility Pattern** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Chain of Responsibility Pattern** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Chain of Responsibility Pattern** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.