Chain of Responsibility
Purpose
Let a request be offered to a sequence of candidate handlers without the sender knowing which one
will deal with it. The sender depends on the chain, not on the handlers, so handlers can be
added, removed and reordered without touching it.
Two shapes travel under this name and behave differently:
Classical CoR each handler decides whether to handle or pass. First-match-wins
is common, but a contract may allow handling and continuation.
Fallthrough to the end is a defined outcome.
Pipeline / every stage processes and passes on: filters,
middleware interceptors, Netty handlers, Spring Security's chain.
All stages run unless one short-circuits deliberately.
Most modern uses are the second. Deciding which you are building is the first design step,
because the unhandled case, the ordering rules and the error semantics all differ.
The partial Java examples use Java 17 unless labelled otherwise. Pattern switches over sealed
types are final in Java 21; on Java 17 use an enum switch or explicit dispatch without enabling
preview merely for this pattern. Inspect actual framework versions and target toolchains.
When it is the answer
The set of handlers is open — new ones arrive from other modules,
plugins or configuration
→ Chain. A switch would have to be edited by every contributor.
Order is meaningful and must be configurable
→ Chain, with the order stated explicitly rather than implied.
A request may be handled at different levels of specificity
(tenant rule → product rule → default)
→ Classical CoR, with the default as the last link.
Cross-cutting work must wrap request handling
→ Pipeline — and prefer the framework's, which already has
ordering, error translation and observability.
When it is not
- Three fixed cases you own. A
switch over a sealed type is shorter, exhaustive and
readable; a chain hides the whole decision behind wiring (java-composition-over-inheritance).
- Every handler must run and none may decline. This is the pipeline/middleware variant of CoR;
name its no-short-circuit contract so a handler cannot silently skip required stages.
- The framework already provides it. A hand-rolled chain beside servlet filters or
HandlerInterceptor duplicates ordering and is invisible to the framework's metrics and
tracing.
- Handlers need to know about each other. Then the chain is a workflow with implicit
coupling; make the sequence explicit or use a mediator (
gof-mediator).
- The chain spans services. A sequence of network hops is a workflow or a saga with partial
failure at every step, not this pattern (
distributed-transactions-and-sagas).
Decision rules
IF nothing handles the request
THEN define whether this is a no-op/not-applicable result, a terminal default, or an
error. Silent fallthrough is correct only when the API makes that outcome visible.
IF handler order is expressed as @Order(100), @Order(200)
THEN the ordering rationale exists only in someone's head. Name the
positions (an enum, an explicit list at the composition root) so
the reason survives.
IF a handler both handles and forwards, in a chain designed for
"first match wins"
THEN the two shapes have been mixed and downstream handlers now see a
request that was already handled.
IF a stage mutates shared state and a later stage throws
THEN the request leaves partial effects behind. Either make stages
pure over a context object and apply effects at the end, or define
an applicable transaction/compensation boundary. Deferring effects alone does not make
their final application atomic or idempotent under retry.
IF handlers hold per-request state in fields
THEN a shared chain is not thread-safe. State belongs in the context
object passed along the chain, not in the handler.
IF the chain is assembled at every request
THEN determine whether tenant, capability or request data genuinely changes membership.
Otherwise precompute immutable chains; when it does, cache bounded variants or
measure per-call assembly rather than assuming it is free.
IF a chain is used for validation and stops at the first failure
THEN callers get one problem at a time. Decide deliberately: fail fast,
or collect every violation (java-exception-design).
Modern Java expression
Classical Modern
─────────────────────────────────── ───────────────────────────────────
abstract Handler with a successor a List<Handler> iterated by the
field and setNext() chain owner — order is data, not a
linked structure nobody can see
handler.handle(request) returns Optional<Result> handle(Request),
void and mutates with the chain taking the first
non-empty
pipeline via successor calls Function composition, or the
framework's filter chain
per-request state in ThreadLocal a context record passed along, or
ScopedValue (scoped-values)
A List<Handler> plus stream().flatMap(h -> h.handle(req).stream()).findFirst() expresses
sequential classical CoR with the order visible at the composition root and no successor wiring.
Do not use a parallel stream when later handlers must never execute after the first decision;
ordered result selection does not guarantee exclusive invocation. Keep
the linked form only when a handler must decide how to invoke the rest — wrapping it in a
try/finally, running it on another thread, or skipping it — which is the pipeline shape.
Cross-cutting checks
- Concurrency. A shared chain may be used concurrently, so handlers and their dependencies
need an explicit thread-safety contract; per-request state should travel in the request or a context
object.
ThreadLocal can leak across pooled threads if not cleared and does not automatically
follow arbitrary executor handoffs; ScopedValue (final in Java 25) is suited to immutable
dynamically scoped context, not a general mutable replacement
(scoped-values, thread-sizing-and-virtual-threads).
- Distribution. Chains that process messages must define what a mid-chain failure means for
acknowledgement: a stage that throws after a side effect has been applied, in an at-least-once
system, will re-run the earlier stages on redelivery. Make stages idempotent or apply effects
through an idempotent/transactional commit boundary even if deferred until the end
(
idempotency, delivery-semantics, poison-messages-and-dlq). Cancellation
must also propagate — a chain that ignores an expired deadline keeps working for a caller that
has gone (cancellation-and-interruption).
- Performance. Cost depends on traversal, dispatch and context design; a context may already
exist and need not be allocated by each link. Important patterns are a chain that computes an expensive value for every
handler to inspect rather than lazily, and a chain long enough that the call site becomes
megamorphic in a hot path (
jit-inlining-and-escape-analysis).
- Testing. Three distinct tests. Each handler alone, with a trivial context. The chain's
order, asserting that a request matching two handlers reaches the intended one. And the
unhandled case, asserting the defined behaviour — the test most often missing, and the one that
catches a silent drop.
Review checklist
References
- Chain against pipeline — the two shapes with their differing
contracts, ordering discipline and how to make it survive contributors, unhandled-request
policies, error propagation and partial state, and the framework equivalents worth using
instead. Read before assembling a chain.
- Worked example — a payment-authorisation rule chain replacing a
branching method: the first-match version, the ordering made explicit, the terminal default,
what happened when a stage acquired a side effect, and the three tests. Read when implementing.
1---2name: gof-chain-of-responsibility3description: Chain of Responsibility in modern Java, and the pipeline it is usually confused with: the classical first-accepting form versus middleware where stages may all process and forward conditionally. Covers choosing between them, the unhandled-request policy that silent chains get wrong, ordering discipline when handlers are contributed independently, error propagation and partial state when a stage throws mid-chain, and why servlet filters and interceptor chains are this pattern already implemented. Use when a request must be offered to several possible handlers, when @Order values are tuned to make a chain work, when a request falls off the end of a chain and nothing happens, or when a chain is proposed for three fixed cases. Does not cover the security framework's own filter configuration, the retry and timeout policies applied around a call (gof-decorator, circuit-breakers), or message processing across services (streaming-pipeline-topologies).4---56# Chain of Responsibility78## Purpose910Let a request be offered to a sequence of candidate handlers without the sender knowing which one11will deal with it. The sender depends on the chain, not on the handlers, so handlers can be12added, removed and reordered without touching it.1314Two shapes travel under this name and behave differently:1516```text17Classical CoR each handler decides whether to handle or pass. First-match-wins18 is common, but a contract may allow handling and continuation.19 Fallthrough to the end is a defined outcome.2021Pipeline / every stage processes and passes on: filters,22middleware interceptors, Netty handlers, Spring Security's chain.23 All stages run unless one short-circuits deliberately.24```2526Most modern uses are the second. Deciding which you are building is the first design step,27because the unhandled case, the ordering rules and the error semantics all differ.28The partial Java examples use Java 17 unless labelled otherwise. Pattern switches over sealed29types are final in Java 21; on Java 17 use an enum switch or explicit dispatch without enabling30preview merely for this pattern. Inspect actual framework versions and target toolchains.3132## When it is the answer3334```text35The set of handlers is open — new ones arrive from other modules,36plugins or configuration37 → Chain. A switch would have to be edited by every contributor.3839Order is meaningful and must be configurable40 → Chain, with the order stated explicitly rather than implied.4142A request may be handled at different levels of specificity43(tenant rule → product rule → default)44 → Classical CoR, with the default as the last link.4546Cross-cutting work must wrap request handling47 → Pipeline — and prefer the framework's, which already has48 ordering, error translation and observability.49```5051## When it is not5253- **Three fixed cases you own.** A `switch` over a sealed type is shorter, exhaustive and54 readable; a chain hides the whole decision behind wiring (`java-composition-over-inheritance`).55- **Every handler must run and none may decline.** This is the pipeline/middleware variant of CoR;56 name its no-short-circuit contract so a handler cannot silently skip required stages.57- **The framework already provides it.** A hand-rolled chain beside servlet filters or58 `HandlerInterceptor` duplicates ordering and is invisible to the framework's metrics and59 tracing.60- **Handlers need to know about each other.** Then the chain is a workflow with implicit61 coupling; make the sequence explicit or use a mediator (`gof-mediator`).62- **The chain spans services.** A sequence of network hops is a workflow or a saga with partial63 failure at every step, not this pattern (`distributed-transactions-and-sagas`).6465## Decision rules6667```text68IF nothing handles the request69THEN define whether this is a no-op/not-applicable result, a terminal default, or an70 error. Silent fallthrough is correct only when the API makes that outcome visible.7172IF handler order is expressed as @Order(100), @Order(200)73THEN the ordering rationale exists only in someone's head. Name the74 positions (an enum, an explicit list at the composition root) so75 the reason survives.7677IF a handler both handles and forwards, in a chain designed for78"first match wins"79THEN the two shapes have been mixed and downstream handlers now see a80 request that was already handled.8182IF a stage mutates shared state and a later stage throws83THEN the request leaves partial effects behind. Either make stages84 pure over a context object and apply effects at the end, or define85 an applicable transaction/compensation boundary. Deferring effects alone does not make86 their final application atomic or idempotent under retry.8788IF handlers hold per-request state in fields89THEN a shared chain is not thread-safe. State belongs in the context90 object passed along the chain, not in the handler.9192IF the chain is assembled at every request93THEN determine whether tenant, capability or request data genuinely changes membership.94 Otherwise precompute immutable chains; when it does, cache bounded variants or95 measure per-call assembly rather than assuming it is free.9697IF a chain is used for validation and stops at the first failure98THEN callers get one problem at a time. Decide deliberately: fail fast,99 or collect every violation (java-exception-design).100```101102## Modern Java expression103104```text105Classical Modern106─────────────────────────────────── ───────────────────────────────────107abstract Handler with a successor a List<Handler> iterated by the108field and setNext() chain owner — order is data, not a109 linked structure nobody can see110111handler.handle(request) returns Optional<Result> handle(Request),112void and mutates with the chain taking the first113 non-empty114115pipeline via successor calls Function composition, or the116 framework's filter chain117118per-request state in ThreadLocal a context record passed along, or119 ScopedValue (scoped-values)120```121122A `List<Handler>` plus `stream().flatMap(h -> h.handle(req).stream()).findFirst()` expresses123sequential classical CoR with the order visible at the composition root and no successor wiring.124Do not use a parallel stream when later handlers must never execute after the first decision;125ordered result selection does not guarantee exclusive invocation. Keep126the linked form only when a handler must decide _how_ to invoke the rest — wrapping it in a127try/finally, running it on another thread, or skipping it — which is the pipeline shape.128129## Cross-cutting checks130131- **Concurrency.** A shared chain may be used concurrently, so handlers and their dependencies132 need an explicit thread-safety contract; per-request state should travel in the request or a context133 object. `ThreadLocal` can leak across pooled threads if not cleared and does not automatically134 follow arbitrary executor handoffs; `ScopedValue` (final in Java 25) is suited to immutable135 dynamically scoped context, not a general mutable replacement136 (`scoped-values`, `thread-sizing-and-virtual-threads`).137- **Distribution.** Chains that process messages must define what a mid-chain failure means for138 acknowledgement: a stage that throws after a side effect has been applied, in an at-least-once139 system, will re-run the earlier stages on redelivery. Make stages idempotent or apply effects140 through an idempotent/transactional commit boundary even if deferred until the end141 (`idempotency`, `delivery-semantics`, `poison-messages-and-dlq`). Cancellation142 must also propagate — a chain that ignores an expired deadline keeps working for a caller that143 has gone (`cancellation-and-interruption`).144- **Performance.** Cost depends on traversal, dispatch and context design; a context may already145 exist and need not be allocated by each link. Important patterns are a chain that computes an expensive value for every146 handler to inspect rather than lazily, and a chain long enough that the call site becomes147 megamorphic in a hot path (`jit-inlining-and-escape-analysis`).148- **Testing.** Three distinct tests. Each handler alone, with a trivial context. The chain's149 order, asserting that a request matching two handlers reaches the intended one. And the150 unhandled case, asserting the defined behaviour — the test most often missing, and the one that151 catches a silent drop.152153## Review checklist154155- [ ] The shape is stated: first-match-wins, or every-stage-runs156- [ ] The unhandled outcome is defined and covered by a test157- [ ] Order is expressed as an explicit list or named positions, not bare numbers158- [ ] Handlers hold no per-request state in fields159- [ ] Partial effects, final commit failure and redelivery have explicit transaction/idempotency/recovery contracts160- [ ] Chain assembly lifetime matches actual variability and is measured/cached when request-specific161- [ ] Deadlines and cancellation propagate through the chain162- [ ] The framework's own chain was considered for cross-cutting concerns163- [ ] A closed set was compared with an exhaustive switch; chain ordering/composition still has a stated benefit164165## References166167- [Chain against pipeline](references/chain-vs-pipeline.md) — the two shapes with their differing168 contracts, ordering discipline and how to make it survive contributors, unhandled-request169 policies, error propagation and partial state, and the framework equivalents worth using170 instead. Read before assembling a chain.171- [Worked example](references/worked-example.md) — a payment-authorisation rule chain replacing a172 branching method: the first-match version, the ordering made explicit, the terminal default,173 what happened when a stage acquired a side effect, and the three tests. Read when implementing.