All chapters in this track
- 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
- 02Chapter 2: Patterns and SOLID: How They Relate
- 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
- 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
- 05Chapter 5: Structural Patterns I: Adapter & Facade
- 06Chapter 6: Structural Patterns II: Decorator & Proxy
- 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
- 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
- 09Chapter 9: Behavioral Patterns II: Observer & Mediator
- 10Chapter 10: Behavioral Patterns III: Command & Memento
- 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
- 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
- 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
- 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
- 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
- 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
- 17Chapter 17: Architectural Patterns III: The Repository Pattern
- 18Chapter 18: Recognizing Which Pattern Fits an Interview Design Prompt
Chapter 11 of 18
Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
State
The problem it solves: an object's behavior should change completely based on its internal state, without a scattered set of conditionals checking that state everywhere the behavior is needed. This pattern received a full worked example already — the elevator controller in the companion OOP book's Chapter 17 — because it's one of the clearest, most concrete demonstrations of polymorphism solving a real design problem. Rather than repeat that example, here's the pattern in its most compact, general form, applied to a different domain: an order's lifecycle.
interface OrderState {
ship(order: Order): void;
cancel(order: Order): void;
name(): string;
}
class PendingState implements OrderState {
name(): string { return "Pending"; }
ship(order: Order): void { order.setState(new ShippedState()); }
cancel(order: Order): void { order.setState(new CancelledState()); }
}
class ShippedState implements OrderState {
name(): string { return "Shipped"; }
ship(): void { throw new Error("Already shipped"); }
cancel(): void { throw new Error("Cannot cancel a shipped order"); } // a structurally-enforced invariant
}
class CancelledState implements OrderState {
name(): string { return "Cancelled"; }
ship(): void { throw new Error("Cannot ship a cancelled order"); }
cancel(): void { throw new Error("Already cancelled"); }
}
class Order {
private state: OrderState = new PendingState();
setState(state: OrderState): void { this.state = state; }
ship(): void { this.state.ship(this); }
cancel(): void { this.state.cancel(this); }
currentState(): string { return this.state.name(); }
}
from abc import ABC, abstractmethod
class OrderState(ABC):
@abstractmethod
def ship(self, order: "Order") -> None: ...
@abstractmethod
def cancel(self, order: "Order") -> None: ...
class PendingState(OrderState):
def ship(self, order: "Order") -> None:
order.set_state(ShippedState())
def cancel(self, order: "Order") -> None:
order.set_state(CancelledState())
class ShippedState(OrderState):
def ship(self, order: "Order") -> None:
raise ValueError("Already shipped")
def cancel(self, order: "Order") -> None:
raise ValueError("Cannot cancel a shipped order")
When to use it: an object's allowed operations genuinely depend on which state it's in, especially when invalid transitions need to be structurally prevented rather than merely checked for — order lifecycles, connection states, workflow/approval steps, the elevator example.
When to avoid it: only two states exist with trivial, stable logic (a plain boolean flag and an if is simpler) — State earns its complexity when there are three or more states and/or the transition rules are non-trivial.
Complexity cost: one class per state — this scales linearly with state count, which is usually a good trade against the alternative (a single class with a combinatorial tangle of conditionals checking a status field everywhere behavior is needed).
Chain of Responsibility
The problem it solves: a request needs to be handled by one of several possible handlers, but the sender shouldn't need to know which handler will end up handling it — each handler gets a chance to process the request or pass it along to the next one in the chain.
Before — the caller must know and check every possible handler directly:
function handleSupportTicket(ticket: { severity: string }): void {
if (ticket.severity === "low") {
console.log("Handled by Tier1Support");
} else if (ticket.severity === "medium") {
console.log("Handled by Tier2Support");
} else if (ticket.severity === "critical") {
console.log("Handled by Tier3Support");
}
// A new severity level, or a change to escalation rules, means editing this function directly.
}
After — Chain of Responsibility lets each handler decide independently whether to handle or pass along:
abstract class SupportHandler {
private next: SupportHandler | null = null;
setNext(handler: SupportHandler): SupportHandler {
this.next = handler;
return handler; // enables fluent chaining: a.setNext(b).setNext(c)
}
handle(ticket: { severity: string }): void {
if (this.canHandle(ticket)) {
this.process(ticket);
} else if (this.next) {
this.next.handle(ticket); // pass along — this handler doesn't need to know who's next, only that someone might be
} else {
console.log("No handler available for this ticket");
}
}
protected abstract canHandle(ticket: { severity: string }): boolean;
protected abstract process(ticket: { severity: string }): void;
}
class Tier1Support extends SupportHandler {
protected canHandle(ticket: { severity: string }): boolean { return ticket.severity === "low"; }
protected process(): void { console.log("Handled by Tier1Support"); }
}
class Tier2Support extends SupportHandler {
protected canHandle(ticket: { severity: string }): boolean { return ticket.severity === "medium"; }
protected process(): void { console.log("Handled by Tier2Support"); }
}
class Tier3Support extends SupportHandler {
protected canHandle(ticket: { severity: string }): boolean { return ticket.severity === "critical"; }
protected process(): void { console.log("Handled by Tier3Support"); }
}
const tier1 = new Tier1Support();
tier1.setNext(new Tier2Support()).setNext(new Tier3Support());
tier1.handle({ severity: "critical" }); // passed along twice, handled by Tier3Support — caller never knew the chain
from abc import ABC, abstractmethod
class SupportHandler(ABC):
def __init__(self) -> None:
self._next: "SupportHandler | None" = None
def set_next(self, handler: "SupportHandler") -> "SupportHandler":
self._next = handler
return handler
def handle(self, ticket: dict) -> None:
if self._can_handle(ticket):
self._process(ticket)
elif self._next:
self._next.handle(ticket)
else:
print("No handler available")
@abstractmethod
def _can_handle(self, ticket: dict) -> bool: ...
@abstractmethod
def _process(self, ticket: dict) -> None: ...
[IN PRACTICE] This is the exact shape behind web framework middleware chains (each middleware either handles the request or calls next()) — recognizing this connection is often the fastest way to internalize Chain of Responsibility, since most engineers have already used it under a different name.
When to use it: multiple potential handlers exist for a request, the sender genuinely shouldn't need to know which one will handle it, and the set of handlers (or their order) may need to change independently of the sending code — support ticket routing, middleware pipelines, event bubbling in UI frameworks.
When to avoid it: there's exactly one handler, or the routing logic is simple enough that a direct if/else (or Strategy, if it's a single decision rather than a sequence of attempts) is clearer — Chain of Responsibility's value is specifically in the sequential, each-gets-a-turn structure, not just "delegating to the right handler."
Complexity cost: debugging can be harder, since tracing which handler ultimately processed a request means walking the chain — worth weighing against the flexibility gained, especially in long or dynamically-constructed chains.
State vs. Chain of Responsibility
[SENIOR INSIGHT] Both involve delegating behavior across multiple objects implementing a shared interface, which occasionally causes confusion — but the shape is different: State has exactly one active handler at a time (the current state), swapped out wholesale on a transition. Chain of Responsibility has multiple handlers in a sequence, each getting a chance, with the request potentially passing through several before (or without) being handled. If you're modeling "what mode am I in right now," that's State. If you're modeling "who among these should deal with this," that's Chain of Responsibility.
Common mistakes
- Using a boolean flag and
iffor a State problem with three or more real states — the conditional complexity that State exists to eliminate creeps back in exactly the way Chapter 6 of the OOP book warned about for polymorphism generally. - Building a long Chain of Responsibility for a single, stable, non-sequential routing decision — that's better modeled as a simple lookup or Strategy; Chain of Responsibility's value is the sequential "try, then pass along" structure specifically.
- Losing track of chain order in a dynamically-constructed Chain of Responsibility, where handler order affects the outcome (a more specific handler needs to come before a catch-all) — this needs to be deliberate and, ideally, tested.
- Not handling the "end of chain, nobody handled it" case, silently dropping requests that no handler claimed — always define explicit behavior for the unhandled case.
Discussion / interview questions
-
"How does State differ from Chain of Responsibility, given both delegate across implementers of a shared interface?" Strong answer: State has exactly one active handler (the current state) at a time, replaced wholesale on transition; Chain of Responsibility has a sequence of handlers, each given a chance to process or pass along a single request.
-
"What's a familiar real-world system that uses Chain of Responsibility, even if it's not called that?" Strong answer: Web framework middleware pipelines — each middleware either handles the request/response or calls the next one in the chain, exactly matching Chain of Responsibility's structure.
-
"When would you use State instead of a status field with conditionals?" Strong answer: When there are three or more meaningfully different states and/or the transition rules are non-trivial or safety-relevant (invalid transitions need to be structurally prevented, not just checked) — ties back to the OOP book's elevator example.
-
"What's a risk specific to a dynamically-constructed Chain of Responsibility?" Strong answer: Handler order affecting correctness (a specific handler needs to run before a general catch-all) combined with harder debugging, since determining which handler ultimately processed a request requires tracing through the chain.
Chapter summary / cheat sheet
- State: one active handler (the current state) at a time, swapped wholesale on transition — eliminates scattered conditionals on a status field, and can structurally prevent invalid transitions.
- Chain of Responsibility: a sequence of handlers, each given a chance to process or pass along a request — the shape behind middleware pipelines.
- Distinguish by asking: "what mode is this object in right now" (State) vs. "who among these should handle this" (Chain of Responsibility).
- Both require a real number of cases/handlers (generally three or more) and real complexity in the rules to justify their cost over a simpler conditional or lookup.