Vasu Patel logoVasu Patel
← Patterns of Good Design
All chapters in this track
  1. 01Chapter 1: Why Patterns Exist — Vocabulary, Not a Checklist
  2. 02Chapter 2: Patterns and SOLID: How They Relate
  3. 03Chapter 3: Creational Patterns I: Factory Method & Abstract Factory
  4. 04Chapter 4: Creational Patterns II: Builder, Singleton, Prototype
  5. 05Chapter 5: Structural Patterns I: Adapter & Facade
  6. 06Chapter 6: Structural Patterns II: Decorator & Proxy
  7. 07Chapter 7: Structural Patterns III: Composite, Bridge & Flyweight
  8. 08Chapter 8: Behavioral Patterns I: Strategy & Template Method
  9. 09Chapter 9: Behavioral Patterns II: Observer & Mediator
  10. 10Chapter 10: Behavioral Patterns III: Command & Memento
  11. 11Chapter 11: Behavioral Patterns IV: State & Chain of Responsibility
  12. 12Chapter 12: Behavioral Patterns V: Iterator & Visitor
  13. 13Chapter 13: Anti-Patterns and Overuse: Recognizing "Pattern Fever"
  14. 14Chapter 14: How Dynamic and Functional Languages Solve These Problems Differently
  15. 15Chapter 15: Architectural Patterns I: MVC/MVVM & Layered/Hexagonal Architecture
  16. 16Chapter 16: Architectural Patterns II: Event-Driven Architecture & CQRS
  17. 17Chapter 17: Architectural Patterns III: The Repository Pattern
  18. 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

  1. Using a boolean flag and if for 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.
  2. 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.
  3. 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.
  4. 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

  1. "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.

  2. "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.

  3. "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.

  4. "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.