Structural · same type, add behavior
Decorator
A is a structural pattern that adds behavior by wrapping an object that already implements the same interface. The wrapper does extra work, then forwards the call. Callers still use the original type.
A concrete example: you send order emails, then product wants a log line, then retry on timeout, then an “[URGENT]” prefix. EmailLoggingRetryUrgentNotifier is not a type. Wrap a Notifier with another Notifier. them. The caller still calls send().
Nested rings — one Notifier all the way down
RetryingNotifier
also retry
LoggingNotifier
also log
UrgentPrefixNotifier
also prefix
EmailNotifier
the real send()
Caller → send() hits the outer ring first. The inner object was passed in, already alive.
Core question
Can I add behavior by wrapping the same interface the caller already uses?
Keep the . Take the . Do extra work, then delegate. Stack on purpose. That is without a subclass for every combination.
The combo explosion you refuse to subclass
Inheritance combos
- EmailNotifier
- LoggingEmailNotifier
- RetryEmailNotifier
- LoggingRetryEmailNotifier
- UrgentLoggingRetryEmailNotifier
2^n classes. A new concern doubles the list.
Three small wrappers
new RetryingNotifier(
new LoggingNotifier(
new UrgentPrefixNotifier(email)),
3)
Order matters: retry-outside-logging logs every attempt; logging-outside-retry logs once per successful send.
Java sketch: logging and retry wrap the same Notifier
interface Notifier {
void send(String to, String body);
}
class EmailNotifier implements Notifier {
public void send(String to, String body) { /* SMTP */ }
}
class LoggingNotifier implements Notifier {
private final Notifier inner;
LoggingNotifier(Notifier inner) { this.inner = inner; }
public void send(String to, String body) {
System.out.println("sending to " + to);
inner.send(to, body);
}
}
class RetryingNotifier implements Notifier {
private final Notifier inner;
private final int maxAttempts; // bound attempts
RetryingNotifier(Notifier inner, int maxAttempts) {
this.inner = inner;
this.maxAttempts = maxAttempts;
}
public void send(String to, String body) {
// Prefer an idempotent send (or idempotency key) so a retry cannot double-deliver.
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
inner.send(to, body);
return;
} catch (RuntimeException e) {
if (!isRetryable(e) || attempt == maxAttempts) throw e;
sleepBackoff(attempt); // optional: exponential backoff + jitter
}
}
}
private static boolean isRetryable(RuntimeException e) {
return e instanceof TransientFailure; // timeouts, 503 — not validation or auth
}
private static void sleepBackoff(int attempt) {
try {
Thread.sleep(Math.min(1000L, 50L << (attempt - 1)));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException(ie);
}
}
}
// Notifier n = new RetryingNotifier(new LoggingNotifier(new EmailNotifier()), 3);Java I/O is the living example
new BufferedInputStream(new FileInputStream(file)). Same InputStream, extra buffering. Compression, encryption, and counting bytes are more layers. Interview twins: logging, retry, caching a get, compressing a payload. See Notification for the product-shaped version.
Decorator is not Adapter, Facade, or Proxy
The class diagram of Decorator and Proxy is almost the same box wrapping a box. Intent is what you are graded on. Adapter and Facade are not even the same diagram.
Decorator — “also do X”
- Same interface as the inner object.
- Inner is constructed first, then handed in.
- Adds a feature the client is glad to stack: log, retry, compress, prefix.
- Nesting three wrappers is normal.
- Does not delay creation, check ACL, or hop the network as its reason to exist.
Proxy — “only if / not yet / over there”
- Same interface — that is where the confusion starts.
- Often owns creation: DiskImage is born on first display().
- Controls access, lifetime, or location — not a new product feature.
- You rarely stack three proxies for logging + retry + prefix.
- A logging wrapper named Proxy is a misnamed decorator.
Adapter — “wrong shape”
StripeClient cannot be Payable without mapping. The wrapper is a different type than the wrappee. If your decorator is no longer a Notifier, you did not extend behavior — you translated types.
Facade — “too many collaborators”
Hides many classes behind one simpler method. A decorator hides nothing: the caller still sees Notifier, and there is still one inner Notifier, not four departments.
How to implement it (four steps)
- 01
Keep the interface
The wrapper is a Notifier — not a type the caller must learn.
- 02
Take the inner object
Constructor injection. Tests wrap a fake. Production wraps the real emailer.
- 03
Do extra work, then delegate
Log, then inner.send. Retry only eligible failures, bound attempts, prefer idempotent sends — then delegate.
- 04
Stack on purpose
Order matters. Say retry-outside-logging out loud.
Common mistakes
A decorator that changes the interface
Calling lazy load a Decorator
One wrapper that logs, retries, and prefixes
Retrying every RuntimeException
Swallowing the real send
How to explain this in an interview
“Retry and logging are extra behavior on the same Notifier, so I will decorate. The emailer already exists and is passed in. This is not a proxy — we are not delaying construction. This is not an adapter — the type is already Notifier.”
Draw three rings around EmailNotifier. Label which ring runs first on send().
Stripe must become Payable, then you want retry. Where does Adapter sit, and where does this page sit?
Takeaway
Decorator means “also do X” on the same type, with a live inner object, stacked on purpose. Wrong shape is Adapter. Too many collaborators is Facade. Not yet / only if / over there is Proxy.