skip to content

In a Chain of Responsibility, how does each handler decide whether to stop or continue, and what is the difference between a 'pure' chain (at most one handler acts) and a 'broadcast' chain (every handler may act)?

level: middleimportance: must knowfreq 55%

answer

  1. guard = applicable?; propagation = continue?
  2. pure chain = first match wins
  3. broadcast = all-of, accumulate context
  4. Boolean / Optional / sealed Continue|Handled|Rejected
  5. mutating the request re-couples downstream

basics

~20 s

Each handler tests whether the request is its business. In a pure chain the first competent handler processes it and stops; in a broadcast chain handlers may act and still forward, so several contribute. The contract must say which it is.

solid answer

~50 s

The stop-or-continue decision is the handler's own: it applies a guard (request type, threshold, permission, feature flag) and then either (a) processes and returns without delegating — terminating the chain — or (b) delegates to the successor. Two variants exist. A **pure** chain grants exclusive responsibility: at most one handler acts, so the first match wins and ordering encodes precedence. A **broadcast/accumulating** chain lets every handler act and keep forwarding — validation collecting all errors, enrichers adding fields, filters logging then continuing. Mixing the two silently is a common defect: one handler assumes exclusivity while another assumes it will always run. Make the contract explicit in the signature — return `Boolean handled`, an `Optional<Result>`, or a sealed `Continue | Stop(result)` outcome — rather than relying on `void` plus convention. Also decide whether a handler may mutate the request before forwarding, since that couples downstream handlers to upstream ones.

code

pseudocode · 12 lines
pseudocode
// explicit outcome removes the ambiguity of void handle()
sealed Outcome { Continue; Handled(result); Rejected(error) }

fun runChain(req, handlers): Outcome {
  for (h in handlers) {
    when (val o = h.handle(req)) {
      is Continue -> continue          // not mine
      else        -> return o          // consumed or rejected: stop
    }
  }
  return Continue                      // fell off the end - caller decides
}

go deeper

for a junior

Say that each handler checks whether the request is its responsibility and then either handles it and stops, or passes it to the next one.

for a middle

Separate the guard from the propagation decision, name the pure (first-match) versus broadcast (all-of) variants, and give an example of each — dispatch versus multi-error validation.

for a senior

Argue for encoding the contract in the return type (Optional or sealed Continue/Handled/Rejected) instead of void plus convention, and cover request mutation, exception policy, and async continuation.

for a principal

Treat the chain contract as an API you are asking many teams to obey: define outcome semantics, exception and re-entrancy policy, and ship conformance tests plus per-stage tracing so that violations surface at build time rather than in an incident.

## The decision point Every handler in a Chain of Responsibility answers two independent questions: 1. **Am I competent/applicable for this request?** — the *guard*, evaluated from the request's own data (type, size, amount, tenant, headers, user role) or from ambient state (feature flag, config, current load). 2. **Having acted (or not), should the request continue down the chain?** — the *propagation* decision. These are separate. "Applicable" does not have to mean "terminal", and "not applicable" does not have to mean "forward" (a handler could legitimately reject and stop). That gives four possible outcomes per handler: | Acted? | Forwarded? | Meaning | |---|---|---| | no | yes | pass-through, not my business | | yes | no | **consume** — classic terminal handling | | yes | yes | **contribute** — enrich/log/validate, let others also act | | no | no | **short-circuit reject** — e.g. auth failure ends the chain with an error | ## Pure chain (exclusive responsibility) Only the first applicable handler acts; it does not delegate. Semantics: *first match wins*. - Ordering encodes **precedence**: put the most specific handler before the more general one, exactly as with `catch` clauses or routing rules. - Useful for: dispatch by request kind, escalation/approval tiers, format parsers, fallback strategies. - Failure mode: a broad handler placed early **shadows** later, more specific ones — they become dead code that tests may never reach. ## Broadcast / accumulating chain Every applicable handler acts and then forwards. Semantics: *all-of*, order affects the composed result but not who gets to run. - Useful for: validation that must report **all** violations rather than the first; request enrichment; cross-cutting concerns like metrics, tracing, and logging; audit trails. - The chain usually carries a mutable **context** (accumulated errors, added attributes) or each stage returns a transformed value. - Failure mode: a handler that mistakenly stops turns a supposed all-of chain into a first-match chain, silently dropping downstream behavior. ## Making the contract explicit A `void handle(request)` signature makes the two variants indistinguishable and pushes the rule into tribal knowledge. Better encodings: - `Boolean handle(req)` — `true` = consumed. Cheap, readable, but says nothing about the produced value. - `Optional<Result> handle(req)` / nullable result — present = handled with this result; the driver stops on the first present value. - A sealed/algebraic outcome: `Continue`, `Handled(result)`, `Rejected(error)`. Most expressive: it distinguishes "not mine" from "mine, and denied", which a boolean conflates. - **Explicit next invocation** (middleware style): the handler receives `next` as a parameter and *chooses* to call it. Continuation is then visible in the code rather than implied by a return value. Whichever you pick, state it once in the Handler interface's documentation and enforce it in tests. ## Mutation of the request A handler may forward the request **unchanged** or **modified**. Modification is powerful (normalization, decoration, adding a resolved user principal) but reintroduces coupling: downstream handlers now depend on upstream ones having run. Safer disciplines: - Pass an immutable request plus a separate, explicitly-typed context that only additive writes touch. - Or return a new transformed request from each stage (pipeline style) so the data flow is visible. ## Edge cases to reason about - **Everyone declines** — the request exits the chain unhandled; the driver, not the handlers, must define the policy (default handler, error, empty result). - **A handler throws** — does the exception abort the chain, or should the driver catch and continue to the next candidate? Continuing hides bugs; aborting can take down a whole request for one optional enricher. Decide per chain and document it. - **Re-entrancy / loops** — a handler that re-submits the request to the chain head can loop forever; guard with a hop count or a "already seen" marker. - **Asynchrony** — with futures/promises, "continue" means returning `next.handle(req)`'s future; forgetting to return it drops the rest of the chain silently. - **Ordering-sensitive short-circuit** — if an authentication handler can reject, it must be before anything that assumes an authenticated principal. ## Testing the decision Per handler: assert (applicable → acted, and the correct propagation outcome) and (not applicable → forwarded untouched). Per chain: assert the first-match precedence for pure chains, and for broadcast chains assert that *all* expected contributors ran even when an early one acted. A test that only checks the happy path of a single handler will not catch a chain wired in the wrong order.

  • How would you keep a validation chain from stopping at the first error?
    Make it a broadcast chain: every validator appends its violations to a shared accumulating context and always forwards; only the driver decides at the end whether the collected list is empty. Do not let a validator return a terminal 'handled' outcome, and add a chain-level test asserting that multiple violations are reported for a request that breaks several rules.
  • A handler in your chain throws an unexpected exception. Should the chain continue?
    It depends on the handler's role, and the policy must be explicit. For a mandatory step (authorization, persistence) the exception should abort the whole chain. For optional cross-cutting enrichers (metrics, non-critical logging) the driver may catch, record the failure, and continue — but only if downstream handlers genuinely do not depend on that stage. Swallowing exceptions by default hides real bugs.

A pure chain is a queue of specialists where the first one qualified takes the case and the file stops moving. A broadcast chain is a document circulating for signatures — everyone who must sign does, and it keeps going.

saying these in an interview costs you the question

  • Assuming every chain is first-match; accumulating/broadcast chains are equally common and behave differently.
  • Using a void handle() signature and relying on convention to know whether a handler consumed the request.
  • Letting a validator throw or consume in a chain that is supposed to collect all violations.
  • Mutating the request in place without documenting it, silently making downstream handlers depend on upstream ones.
  • In async code, calling next without returning or awaiting its future, which drops the rest of the chain.

context