skip to content

What is the Chain of Responsibility design pattern, and what coupling problem between a request's sender and its receiver does it solve?

level: juniorimportance: must knowfreq 70%

answer

  1. sender knows only the head
  2. handle or pass to successor
  3. GoF behavioral; replaces if/else cascade
  4. may end unhandled
  5. middleware, event bubbling, exception catch

basics

~20 s

Chain of Responsibility links several handler objects in a sequence. The sender passes a request to the first handler; each one either handles it or forwards it onward. The sender never knows which handler ultimately responds.

solid answer

~50 s

Chain of Responsibility is a behavioral pattern whose intent is to avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle it. Handlers are arranged in a sequence; the request is passed to the first, and each handler decides to process it, forward it to the successor, or both. The sender holds a reference only to the chain's head (or to an abstract handler interface), not to any concrete handler, so receivers can be added, removed, or reordered without touching the caller. The pattern converts a rigid conditional cascade (`if type A … else if type B …`) into a set of independent, individually testable objects, and lets the chain be composed at runtime — from configuration, dependency injection, or plugin discovery. The price is indirection: the resolved handler is only visible at runtime, and a request may reach the end of the chain unhandled.

code

pseudocode · 10 lines
pseudocode
interface Handler { handle(req): Boolean }   // true = consumed

class AutoApprove(next) implements Handler {
  handle(req) = if (req.amount < 100) { approve(req); true }
                else next?.handle(req) ?: false
}

// sender knows only the head
val chain = AutoApprove(TeamLead(Director(null)))
if (!chain.handle(request)) escalateToHuman(request)  // explicit unhandled policy

go deeper

for a junior

State the intent in one sentence — several handlers get a chance, each handles or forwards, sender knows only the first — and name one everyday example such as HTTP middleware or exception handling.

for a middle

Add the participants (Handler, ConcreteHandler, successor, client), show how it replaces an if/else cascade, and mention that requests can end up unhandled.

for a senior

Discuss list-driven vs linked encodings, runtime composition from configuration/DI, testability of individual handlers, and contrast with Decorator/Strategy/Observer.

for a principal

Frame it as trading compile-time explicitness for runtime configurability, and set the governance around that: who owns ordering, what the unhandled policy is, and what observability makes the runtime-resolved handler visible in production.

## The problem Suppose code must react to a request whose correct receiver depends on the request's content, the caller's privileges, or runtime configuration. The naive shape is a conditional cascade inside the sender: ``` if (request.amount < 100) approveAutomatically(request) else if (request.amount < 10_000) teamLead.approve(request) else if (request.amount < 100_000) director.approve(request) else board.approve(request) ``` This couples the sender to **every** receiver: the sender imports them, knows their order, and must be edited whenever a tier is added, removed, or has its threshold changed. Every receiver's existence is baked into one place, which violates the Open/Closed idea (a module should be extendable without editing it) and makes the sender hard to unit-test in isolation. ## The pattern **Chain of Responsibility** (one of the original "Gang of Four" behavioral patterns) restates the problem: *give several objects a chance at the request and let them decide*. Participants: - **Handler** — an abstraction (interface or abstract base) declaring one operation, e.g. `handle(request)`, and holding an optional reference to a **successor** (the next handler). - **ConcreteHandler** — implements `handle`. It inspects the request; if it is competent and willing, it processes it; otherwise it delegates to the successor. - **Client / sender** — builds or receives the chain and calls `handle` on the **head** only. The key structural fact: the sender's static dependency is on the *Handler abstraction and the head reference*, never on the concrete receivers. Which object actually services the request is decided at runtime, by the chain's contents and order. ## Terms defined - **Coupling** — how much one unit of code must know about another. Here we specifically remove *sender → concrete-receiver* compile-time knowledge. - **Successor / next** — the link each handler holds to the following one; `null`/absent marks the tail. - **Behavioral pattern** — a pattern about how objects distribute responsibility and communicate at runtime (as opposed to creational or structural patterns). - **Runtime composition** — the chain's membership and order can come from a config file, a DI container's ordered list, an annotation-driven priority, or plugin discovery, rather than being hardcoded. ## Shape of a handler Two common encodings: 1. **Linked handlers** — each handler stores `next` and explicitly calls `next.handle(request)`. Classic GoF. 2. **List-driven dispatcher** — the chain is a list; a small driver loops over it calling each handler until one reports "handled". Same semantics, less boilerplate, easier to log/instrument, and the handlers no longer need a mutable `next` field. Both satisfy the intent. Frameworks usually choose (2) internally while presenting (1) conceptually. ## What you gain - **Decoupling** — senders don't name receivers. - **Open for extension** — add a handler by adding a class + a registration; no edits to the sender or to sibling handlers. - **Single responsibility per handler** — each is small and unit-testable in isolation (feed it a request, assert handled/not-handled and any side effect). - **Dynamic reconfiguration** — enable/disable/reorder handlers per environment, tenant, or feature flag. ## What you pay - **No guarantee of service** — the request may traverse the whole chain and be handled by nobody. The pattern's own definition tolerates this, so the design must decide explicitly what "fell off the end" means (default handler, thrown error, returned `Optional`/absent result). - **Runtime-only visibility** — you cannot read the source and know who responds; you need logging or a debugger. Stack traces get deep with linked handlers. - **Order becomes an invisible dependency** — correctness can hinge on relative order that no type system enforces. - **Cost** — a long chain runs many predicates per request; usually negligible, occasionally not on a hot path. ## Where you have already met it - HTTP server **middleware / filter chains** (authentication → logging → rate limiting → routing). - **Exception handling** in most languages: the throw travels up the call stack until some `catch` claims it — a chain the runtime builds for you. - **Event bubbling** in UI toolkits: the event goes child → parent until a listener consumes it. - **Logging hierarchies** where a record climbs from a child logger to ancestors until one has an appender or a handler stops propagation. - **Approval / escalation workflows** and **validation pipelines**. ## Distinguishing it from neighbours - **Strategy**: exactly one interchangeable algorithm chosen by the client. CoR: *many* candidates, chosen by themselves, in order. - **Decorator**: structurally identical (wrappers around a next reference) but its intent is that *every* wrapper contributes behavior and the core always runs; CoR intends that processing may stop early and the core may never be reached. - **Observer**: broadcast to all subscribers with no ordering contract and no "consume" semantics; CoR is sequential and typically stops at the first taker. - **Command**: encapsulates *what to do* as an object; often the thing being passed *along* a chain. ## Rule of thumb Reach for Chain of Responsibility when (a) more than one object could legitimately handle a request, (b) the set of handlers should change without editing the caller, and (c) you can state a sensible policy for the unhandled case. If exactly one receiver is always correct and known, a direct call or a lookup map is simpler and more honest.

  • How is Chain of Responsibility different from Decorator, given both wrap a 'next' reference?
    Structure is nearly the same; intent differs. Decorator layers additional behavior and always delegates to the wrapped core, so every layer contributes. Chain of Responsibility gives each link the right to consume the request and stop, so downstream links — including any 'core' — may never run.
  • Does the client have to know the chain's order?
    The client that *invokes* the chain does not; it only holds the head. But someone must own the order — a builder, DI configuration, or priority metadata. That configuration point becomes the place where ordering knowledge lives, and it should be explicit and tested.

A support ticket at a company: it lands on the front desk, who either resolves it or escalates it to tier 2, then tier 3, then engineering. The customer never picks the person who answers — they only know where to drop the ticket.

saying these in an interview costs you the question

  • Saying the sender picks the handler — that is the coupling the pattern removes.
  • Claiming the pattern guarantees some handler will process the request; it explicitly does not.
  • Confusing it with Observer: a chain is sequential with consume semantics, not a broadcast to all subscribers.
  • Treating it as identical to Decorator because both hold a 'next' — the intents differ on whether the core always runs.
  • Describing it as a way to make code faster; it adds indirection and a linear scan.

context