Decorator
Purpose
Add behaviour to one object without changing its type, and let several such additions compose.
The defining property is that the wrapper implements the same interface as what it wraps — which
is what makes the layers stackable, and what makes their order meaningful.
Order is not a detail. Retry outside timeout and timeout outside retry are both reasonable
designs with different semantics, and a stack assembled without deciding which one is intended
will behave in whichever way the wiring happened to produce.
When it is the answer
Behaviour must be added to some instances and not others, chosen at
wiring time
→ Decorator. Inheritance would decide it at compile time.
Several independent additions must combine, and combinations
multiply (retry × cache × metrics × tracing)
→ Decorator. Subclasses would be the product; wrappers are the sum.
The addition is cross-cutting and the interface is stable
→ Decorator — or the framework's own mechanism, which is the
same pattern already implemented (see below).
When it is not
- The wrapper changes the interface. That is an Adapter (
gof-adapter).
- The primary intent is substituting for another object while controlling access — lazy
loading, remoting or access checks. That is usually Proxy. Both patterns commonly implement the
same interface and may be structurally identical, so classify by responsibility (
gof-proxy).
- Only one stable combination is ever used. A composed class may make the call graph easier to
inspect, but separate decorators can remain worthwhile for independent ownership, testing or
framework integration. Compare change coupling rather than counting combinations.
- The framework already provides it. Servlet filters,
HandlerInterceptor, Spring AOP
advice, RestClient request interceptors, Micrometer instrumentation and Resilience4j
decorators already provide composition mechanisms. Verify their ordering, async-context and
observability semantics; hand-rolling beside them otherwise puts policy in two places.
- Behaviour differs by the object's state. That is State (
gof-state).
Ordering is semantics
Examples are partial Java 17 unless a framework is named. Inspect the project's resolved framework,
HTTP provider and instrumentation versions; no decorator choice authorizes upgrades or dependencies.
Read a stack outermost-first. Each layer sees the one below it as
"the call".
Metrics( ← counts logical operations, one per caller request
CircuitBreaker( ← opens on the outcome of whole operations
Retry( ← its attempts are invisible to the breaker above
Timeout( ← bounds ONE attempt
Client)))))
Metrics(
Retry(
CircuitBreaker( ← sees each attempt; reject open-breaker failures from retry eligibility
Timeout(
Client))))
| Arrangement |
Meaning |
Choose when |
| Timeout inside Retry |
Per-attempt bound plus backoff/queueing in total |
Pair with remaining caller budget |
| Timeout outside Retry |
Outer completion bound; inner work must honor deadline |
Propagate cancellation and remaining time |
| Cache outside Retry |
A valid hit avoids downstream retries |
Hit semantics and cache failure policy permit it |
| Cache inside Retry |
Each attempt consults cache; concurrent fill may matter |
Explicit cache/load/concurrency contract |
| Breaker outside Retry |
The breaker sees logical operations |
Normal |
| Breaker inside Retry |
Breaker counts attempts; rejection must not be retried |
Attempt-level failure isolation is intended |
| Metrics outside everything |
Latency includes retries — the caller's true experience |
Usually retain as logical-operation telemetry |
| Metrics inside Retry |
Per-attempt counts and error rates |
In addition, under a different metric name |
A common starting point is logical metrics → propagated deadline/budget → breaker → retry →
per-attempt timeout → client, with separate attempt telemetry. It is not universal: breaker
placement decides whether it counts attempts or logical failures, and the retry must derive each
attempt budget from remaining time. Document and test the selected semantics because the type
system does not record them.
Decision rules
IF retries exist at more than one layer of the system
THEN attempts can multiply: 3 at the client × 3 at the gateway = 9 requests
to a struggling dependency. Prefer one owner per failure domain; multiple layers
require a shared attempt/deadline budget and evidence that they do not amplify
(retries-and-backoff, cascading-failures).
IF a retry decorator wraps a non-idempotent operation
THEN it can duplicate side effects. Require provider-enforced idempotency within its scope,
matching parameters/retention, or an independently safe operation; a key alone proves nothing.
IF callers use ==, instanceof or equals on the decorated object
THEN inspect the actual identity/equality contract. Interface instanceof still works; concrete
checks may fail. Preserve registration identity; avoid automatic equality forwarding or an
unrestricted unwrap path that bypasses access, transaction or lifecycle policy.
IF the decorator holds state — a cache, a counter, a breaker
THEN the composed object is stateful and shared. Its thread safety is
now the decorator's responsibility, not the delegate's.
IF the framework has a mechanism for this concern
THEN prefer it when it satisfies the contract; verify ordering, metrics and tracing configuration.
Custom composition remains valid when integrated explicitly or the framework cannot fit.
IF the stack obscures call order, context propagation or failure attribution
THEN make wiring observable, collapse inseparable policies, or use a framework chain.
Depth alone is not the decision criterion.
IF a decorator swallows or translates the delegate's exceptions
THEN it is changing the contract, not decorating it. State that
explicitly; it is the layer most likely to hide an outage.
Cross-cutting checks
- Concurrency. A decorator over a stateless, thread-safe delegate can make the composition
unsafe: a counter, a cache, an
HashMap of in-flight keys, a non-atomic read-modify-write of a
breaker's state. Each stateful layer needs its own memory-model argument. Conversely,
safety can also come from confinement or separate owned instances; synchronization must cover
all conflicting access, including aliases outside the wrapper (java-memory-model).
- Distribution. This is where resilience layers live, so the ordering table above is a
production concern rather than a stylistic one. Two failures dominate: retry amplification
across layers, which converts a partial outage into a full one; and a timeout placed so that
the total call time exceeds the caller's deadline, so the caller gives up while the work
continues (
timeouts-and-deadlines, cascading-failures).
- Performance. Each layer adds a dispatch opportunity that HotSpot may inline at stable call
sites. Costs that often matter more are allocation per call inside a
layer (a new context object, a lambda capturing state, a
String built for a log line that is
then discarded), and lost inlining once the call site is megamorphic
(jit-inlining-and-escape-analysis).
- Testing. Test each decorator against a fake delegate — that is the pattern's dividend. Then
write one test for the composed stack that asserts the order: that a timeout during a retry
produces N attempts, that a cache hit performs zero calls. Order is the property nothing else
checks, and it is the one that regresses when someone reorders the wiring.
Review checklist
References
- Ordering and composition — every common layer pair
with its semantics, retry amplification arithmetic, deadline propagation through a stack,
identity loss and unwrapping (
java.sql.Wrapper, AOP proxies, listener deregistration), and
when a framework interceptor should replace a hand-rolled decorator. Read before assembling or
reordering a stack.
- Worked example — an outbound pricing client decorated for
metrics, breaking, retry, timeout and caching: the wiring with its order justified, the
per-layer tests, the order test, and an illustrative amplification scenario. Read when
implementing.
1---2name: gof-decorator3description: Decorator in modern Java: wrapping an object in something of its own interface to add behaviour, stackably, at runtime — and the fact that the stacking order changes the semantics. Covers the ordering of retry, timeout, circuit breaker, cache, metrics and logging and what each arrangement means, retry amplification across layers, the identity loss that breaks ==, instanceof and listener deregistration, when a framework interceptor is the same pattern already provided, and the thread-safety a stateful decorator introduces. Use when resilience or observability layers are added around a client, when a wrapper chain is reordered, when a decorated object fails an instanceof check, when retries appear at two levels, or when a wrapper is proposed that changes the interface. Does not cover changing an interface (gof-adapter), controlling access to an object (gof-proxy), one entry point over a subsystem (gof-facade), or the retry and timeout policies themselves (circuit-breakers, retries-and-backoff).4---56# Decorator78## Purpose910Add behaviour to one object without changing its type, and let several such additions compose.11The defining property is that the wrapper implements the same interface as what it wraps — which12is what makes the layers stackable, and what makes their order meaningful.1314Order is not a detail. Retry outside timeout and timeout outside retry are both reasonable15designs with different semantics, and a stack assembled without deciding which one is intended16will behave in whichever way the wiring happened to produce.1718## When it is the answer1920```text21Behaviour must be added to some instances and not others, chosen at22wiring time23 → Decorator. Inheritance would decide it at compile time.2425Several independent additions must combine, and combinations26multiply (retry × cache × metrics × tracing)27 → Decorator. Subclasses would be the product; wrappers are the sum.2829The addition is cross-cutting and the interface is stable30 → Decorator — or the framework's own mechanism, which is the31 same pattern already implemented (see below).32```3334## When it is not3536- **The wrapper changes the interface.** That is an Adapter (`gof-adapter`).37- **The primary intent is substituting for another object while controlling access** — lazy38 loading, remoting or access checks. That is usually Proxy. Both patterns commonly implement the39 same interface and may be structurally identical, so classify by responsibility (`gof-proxy`).40- **Only one stable combination is ever used.** A composed class may make the call graph easier to41 inspect, but separate decorators can remain worthwhile for independent ownership, testing or42 framework integration. Compare change coupling rather than counting combinations.43- **The framework already provides it.** Servlet filters, `HandlerInterceptor`, Spring AOP44 advice, `RestClient` request interceptors, Micrometer instrumentation and Resilience4j45 decorators already provide composition mechanisms. Verify their ordering, async-context and46 observability semantics; hand-rolling beside them otherwise puts policy in two places.47- **Behaviour differs by the object's state.** That is State (`gof-state`).4849## Ordering is semantics5051Examples are partial Java 17 unless a framework is named. Inspect the project's resolved framework,52HTTP provider and instrumentation versions; no decorator choice authorizes upgrades or dependencies.5354```text55Read a stack outermost-first. Each layer sees the one below it as56"the call".5758 Metrics( ← counts logical operations, one per caller request59 CircuitBreaker( ← opens on the outcome of whole operations60 Retry( ← its attempts are invisible to the breaker above61 Timeout( ← bounds ONE attempt62 Client)))))6364 Metrics(65 Retry(66 CircuitBreaker( ← sees each attempt; reject open-breaker failures from retry eligibility67 Timeout(68 Client))))69```7071| Arrangement | Meaning | Choose when |72| ------------------------------ | ------------------------------------------------------- | ------------------------------------------------ |73| Timeout **inside** Retry | Per-attempt bound plus backoff/queueing in total | Pair with remaining caller budget |74| Timeout **outside** Retry | Outer completion bound; inner work must honor deadline | Propagate cancellation and remaining time |75| Cache **outside** Retry | A valid hit avoids downstream retries | Hit semantics and cache failure policy permit it |76| Cache **inside** Retry | Each attempt consults cache; concurrent fill may matter | Explicit cache/load/concurrency contract |77| Breaker **outside** Retry | The breaker sees logical operations | Normal |78| Breaker **inside** Retry | Breaker counts attempts; rejection must not be retried | Attempt-level failure isolation is intended |79| Metrics **outside** everything | Latency includes retries — the caller's true experience | Usually retain as logical-operation telemetry |80| Metrics **inside** Retry | Per-attempt counts and error rates | In addition, under a different metric name |8182A common starting point is **logical metrics → propagated deadline/budget → breaker → retry →83per-attempt timeout → client**, with separate attempt telemetry. It is not universal: breaker84placement decides whether it counts attempts or logical failures, and the retry must derive each85attempt budget from remaining time. Document and test the selected semantics because the type86system does not record them.8788## Decision rules8990```text91IF retries exist at more than one layer of the system92THEN attempts can multiply: 3 at the client × 3 at the gateway = 9 requests93 to a struggling dependency. Prefer one owner per failure domain; multiple layers94 require a shared attempt/deadline budget and evidence that they do not amplify95 (retries-and-backoff, cascading-failures).9697IF a retry decorator wraps a non-idempotent operation98THEN it can duplicate side effects. Require provider-enforced idempotency within its scope,99 matching parameters/retention, or an independently safe operation; a key alone proves nothing.100101IF callers use ==, instanceof or equals on the decorated object102THEN inspect the actual identity/equality contract. Interface instanceof still works; concrete103 checks may fail. Preserve registration identity; avoid automatic equality forwarding or an104 unrestricted unwrap path that bypasses access, transaction or lifecycle policy.105106IF the decorator holds state — a cache, a counter, a breaker107THEN the composed object is stateful and shared. Its thread safety is108 now the decorator's responsibility, not the delegate's.109110IF the framework has a mechanism for this concern111THEN prefer it when it satisfies the contract; verify ordering, metrics and tracing configuration.112 Custom composition remains valid when integrated explicitly or the framework cannot fit.113114IF the stack obscures call order, context propagation or failure attribution115THEN make wiring observable, collapse inseparable policies, or use a framework chain.116 Depth alone is not the decision criterion.117118IF a decorator swallows or translates the delegate's exceptions119THEN it is changing the contract, not decorating it. State that120 explicitly; it is the layer most likely to hide an outage.121```122123## Cross-cutting checks124125- **Concurrency.** A decorator over a stateless, thread-safe delegate can make the composition126 unsafe: a counter, a cache, an `HashMap` of in-flight keys, a non-atomic read-modify-write of a127 breaker's state. Each stateful layer needs its own memory-model argument. Conversely,128 safety can also come from confinement or separate owned instances; synchronization must cover129 all conflicting access, including aliases outside the wrapper (`java-memory-model`).130- **Distribution.** This is where resilience layers live, so the ordering table above is a131 production concern rather than a stylistic one. Two failures dominate: retry amplification132 across layers, which converts a partial outage into a full one; and a timeout placed so that133 the total call time exceeds the caller's deadline, so the caller gives up while the work134 continues (`timeouts-and-deadlines`, `cascading-failures`).135- **Performance.** Each layer adds a dispatch opportunity that HotSpot may inline at stable call136 sites. Costs that often matter more are allocation per call inside a137 layer (a new context object, a lambda capturing state, a `String` built for a log line that is138 then discarded), and lost inlining once the call site is megamorphic139 (`jit-inlining-and-escape-analysis`).140- **Testing.** Test each decorator against a fake delegate — that is the pattern's dividend. Then141 write one test for the composed stack that asserts the _order_: that a timeout during a retry142 produces N attempts, that a cache hit performs zero calls. Order is the property nothing else143 checks, and it is the one that regresses when someone reorders the wiring.144145## Review checklist146147- [ ] The wrapper implements the same interface as what it wraps148- [ ] The stacking order is deliberate and documented at the wiring site149- [ ] Retry ownership and the shared attempt/deadline budget prevent cross-layer amplification150- [ ] Retry safety is established by the operation/provider contract, not merely the presence of a key151- [ ] The total time of the stack fits the caller's deadline152- [ ] Stateful layers state their thread-safety guarantee153- [ ] Identity-sensitive behavior is eliminated, explicitly delegated, or exposed through a154 constrained standard unwrap contract rather than concrete-type assumptions155- [ ] No decorator silently swallows or reclassifies the delegate's failures156- [ ] A test asserts the composed order, not only each layer alone157158## References159160- [Ordering and composition](references/ordering-and-composition.md) — every common layer pair161 with its semantics, retry amplification arithmetic, deadline propagation through a stack,162 identity loss and unwrapping (`java.sql.Wrapper`, AOP proxies, listener deregistration), and163 when a framework interceptor should replace a hand-rolled decorator. Read before assembling or164 reordering a stack.165- [Worked example](references/worked-example.md) — an outbound pricing client decorated for166 metrics, breaking, retry, timeout and caching: the wiring with its order justified, the167 per-layer tests, the order test, and an illustrative amplification scenario. Read when168 implementing.