- Low Level Design
- /
- Decorator Pattern
Course content
The Decorator pattern wraps an object in another object that has the same interface and adds behaviour around it. The wrapper holds the wrapped object as a field and forwards calls to it; before, after, or instead of the forwarded call, it does the extra work: log the call, retry it, cache the result, encrypt the payload, check authorization. Because the wrapper is the same type as the wrapped object, callers cannot tell the difference. Multiple decorators stack: Retrying(Logging(Caching(Email))) is a four-layer chain where each layer adds one concern, and the outermost behaves like a Notifier to the rest of the codebase.
The shape is what makes it powerful. Each decorator is a small class with one job, and you compose them at wiring time to get the exact mix you want for each environment: retries in production, verbose logging in staging, caching in tests. If you have ever built a stream like new BufferedInputStream(new GZIPInputStream(new FileInputStream(path))), you have already stacked three decorators. The page starts from the design this pattern replaces: a subclass per combination of features.
Three independent concerns (which channel, should we log, should we retry) are tangled into a single inheritance tree. Each new combination needs its own subclass. With three concerns and two channels you already have eight classes; add encryption, audit logging, or rate limiting and the count doubles each time. Most of those classes are nearly empty; they exist only to thread one new behaviour through an existing chain.
Worse, the inheritance is fragile. RetryingLoggingEmailNotifier extends LoggingEmailNotifier works for the email channel, but to get the same combination on Slack you write RetryingLoggingSlackNotifier extends LoggingSlackNotifier. You cannot reuse the retry logic across channels because it is welded into a specific subclass tree. The retry math (loop, sleep, last-exception bookkeeping) lives in two places now, and over time the two copies will drift.
Finally, the order of concerns is baked into the class hierarchy. "Retry around logging" gives you a different chain from "log around retries": the first logs once per attempt, the second logs once per send. With subclasses, swapping the order means writing yet another set of classes (LoggingRetryingEmailNotifier is a different chain from RetryingLoggingEmailNotifier). The ordering decision should be a runtime choice, not a compile-time class hierarchy.
What this costs in production
Retries and logging were each a subclass, so the team had both RetryingLoggingEmailNotifier and LoggingRetryingEmailNotifier and could not remember which logged per attempt. During an outage they shipped the wrong one, and it logged every retry of every failed send. The log pipeline took a downstream outage multiplied by three attempts, filled the disk, and took logging down for the whole service.
The notifier tree is one instance of features multiplying into subclasses. In review, the symptoms look like these.
| Smell | What it looks like | Why a decorator is the answer |
|---|---|---|
| A subclass-per-feature explosion | EmailNotifier, RetryingEmailNotifier, LoggingEmailNotifier, RetryingLoggingEmailNotifier, EncryptedRetryingLoggingEmailNotifier ... | N independent features compose into 2^N subclass combinations; one decorator per feature replaces the explosion with composition |
| A god class with feature flags toggling cross-cutting concerns | class Notifier { boolean retry; int maxRetries; boolean log; boolean encrypt; void send(...) { if (log) ...; if (retry) ...; } } | Each flag is a feature pretending to share a class; pulling them into separate decorators makes each one independently testable |
| Cross-cutting concerns (retry, log, audit, cache) duplicated at every call site | for (int i = 0; i < 3; i++) { try { client.send(...); break; } catch (...) { sleep; } } | The retry loop belongs inside a RetryingClient decorator; the call site shrinks back to one call |
| Subclasses that override one method but otherwise just delegate | class LoggedEmail extends EmailNotifier { void send(t,m) { log(t,m); super.send(t,m); } } | Inheritance ties the augment to one base class; a Decorator that holds a Notifier field works for any Notifier implementation |
| A 'wrapper utility' that takes a Function and produces a wrapped Function | Function<I,O> retry(Function<I,O> f, int n) { ... } | This IS Decorator, just spelled functionally; when the wrapped object has multiple methods, the OO Decorator class is the version that scales |
| Adding a new optional behaviour requires touching the using class | Service had to be modified to add a try/catch retry around its dependency call | If the dependency was injected behind an interface, a RetryingDecorator could be added at wiring time without touching the service |
Three things changed and each one is what unlocks composition.
Every decorator implements Notifier, the same interface as the leaf classes. From the outside, RetryingNotifier(LoggingNotifier(EmailNotifier)) is a Notifier; callers just call send. They cannot tell, and do not need to know, how many decorators are in the chain. New decorators slot in transparently anywhere in the chain.
Each decorator holds the wrapped Notifier as a field, populated by its constructor. Because the field type is the interface (not a concrete class), a single RetryingNotifier works around EmailNotifier, SlackNotifier, or any other Notifier, including another decorator. The retry logic, the logging logic, and the encryption logic each live in exactly one place and apply to any channel.
The combinatorial explosion goes away. Three concerns and two channels gave us eight subclass combinations before; now we have two leaves plus three decorators, five classes, and we pick the combination at wiring time. Adding a fourth concern is one new class, not eight more subclasses. And the chain order is a runtime decision: LoggingNotifier(RetryingNotifier(...)) logs once per send; RetryingNotifier(LoggingNotifier(...)) logs once per attempt. The system supports both without writing different classes for each.
With the refactor in hand, it is worth pinning down what Decorator is not, because four neighbouring ideas share its wrap-an-object shape. The differences come down to what the wrapper changes and where the change comes from.
Decorator vs. Adapter: same shape (one object holds another), opposite intent. An Adapter changes the interface; InputStreamReader wraps an InputStream and exposes it as a Reader, because the wrapped class did not implement the target interface. A Decorator keeps the interface the same and adds behaviour; BufferedReader wraps a Reader and is itself a Reader, just with buffering layered on. If the type changes when you wrap, it is Adapter; if the type stays the same, it is Decorator.
Decorator vs. inheritance: both add behaviour to a base class. Inheritance does it at compile time and at most once per concern. Decorator does it at runtime and composes: the same LoggingNotifier can wrap an EmailNotifier, a SlackNotifier, or a RetryingNotifier depending on what the caller passes in. With inheritance, three independent concerns (retry, log, encrypt) produce 2^3 = 8 combinations, each as its own subclass; with decorators, three classes plus a wiring decision give you the same eight combinations.
Decorator vs. Proxy: same shape, different motivation. A Proxy controls access to the wrapped object: lazy initialisation, remote-invocation marshalling, caching, security checks. A Decorator augments the object's behaviour without altering whether or how the call eventually reaches it. The line is fuzzy in practice (CachingNotifier could be either), and modern Spring uses the term "proxy" liberally for both. If your wrapper is deciding whether to call through (lazy load, security deny, remote network call), it is more proxy-ish; if it is adding work around the call (logging, retries, encrypting the payload), it is more decorator-ish.
Decorator vs. Composite: both compose recursively behind the component's own interface, so a chain of decorators and a tree of composites look alike on a whiteboard. Count the children. A decorator holds exactly one component and adds behaviour on the way through; a composite holds many and combines their answers into one. The GoF book calls a decorator a degenerate composite with only one component, but the intents differ: Decorator adds responsibilities to an object, Composite builds part-whole trees. If your wrapper starts collecting children, it has become a composite; if your composite always has one child and tweaks its result, it is a decorator wearing the wrong name.
The entire java.io library is built on Decorator. InputStream is the interface; FileInputStream and ByteArrayInputStream are concrete leaves; BufferedInputStream, DataInputStream, GZIPInputStream, CipherInputStream, and ObjectInputStream are decorators, each one an InputStream that holds an InputStream and adds one concern. The classic chain new ObjectInputStream(new BufferedInputStream(new FileInputStream(path))) is three layers deep, and the Reader/Writer hierarchy mirrors the same design for characters. The collections utilities do it in one call: Collections.unmodifiableList(list), synchronizedList(list), and checkedList(list, type) each return a decorated List that adds one concern, and they stack.
Spring is Decorator industrialised. @Transactional, @Cacheable, @Async, and @Retryable work by wrapping your bean in a runtime-generated wrapper (a CGLIB or JDK dynamic proxy) that runs the cross-cutting concern and forwards to the real method; multiple annotations on one method produce a stack of wrappers in a documented order. The Servlet Filter chain is the same idea at the HTTP layer, and HttpServletRequestWrapper/HttpServletResponseWrapper are base classes the API ships specifically so filters can decorate requests and responses. Resilience4j has the pattern's name on the tin: Decorators.ofSupplier(s).withRetry(retry).withCircuitBreaker(cb).decorate() returns a Supplier that is a decorator chain over the original.
Python gives the word two meanings, and both are relevant. The @decorator syntax wraps callables at definition time: @functools.lru_cache returns a caching wrapper with the same call signature, and a hand-written retry decorator is a dozen lines. That is the same idea as the GoF pattern applied to functions instead of objects, which is worth saying out loud in an interview. The object form is there too, and it mirrors java.io almost exactly: io.BufferedReader wraps a raw io.FileIO and stays a readable stream, gzip.GzipFile wraps any file-like object and stays file-like, and open() assembles the stack for you.
C++ expresses the same layering through stream buffers (Boost.Iostreams stacks gzip or encryption filters onto a stream the way java.io stacks input streams) and, in modern code, through wrapper callables: a function that takes a std::function, adds retry or timing around it, and returns another std::function with the same signature is a decorator in the functional dialect. Java 8 streams read the same way, with every intermediate operation (filter, map, distinct) taking a stream and returning a stream with one more transformation layered on.
| Pros | Cons |
|---|---|
| N concerns x M base classes is N + M files instead of N x M subclasses | Many small classes can feel scattered; readers need to follow the wiring code to see the actual chain |
| Each decorator is independently testable: pass in a stub of the wrapped interface and assert the decorator's behaviour | Stack traces grow one frame per layer; debugging through a 4-layer chain is tedious |
| Chains compose at runtime: different environments wire different chains (verbose logging in staging, none in prod) without code changes | Order matters and is implicit; getting RetryingNotifier(LoggingNotifier(...)) backwards changes whether you log per-attempt or per-send, and the bug is silent |
| Cross-cutting concerns (retry, log, audit, encrypt, cache) live in one class each; one fix updates every chain that uses them | Equality and identity are tricky: wrapped.equals(decorator) is rarely meaningful, and serialising a decorated object requires care |
| Plays well with dependency injection: Spring/Guice can compose chains via configuration, and Spring's @Transactional/@Cacheable do this automatically | Performance: each layer is one extra method call; for hot paths with many decorators the dispatch cost adds up |
Decorator pays off when the same concern (retry, log, audit) needs to apply to many different leaf objects, or when the same leaf needs different combinations of concerns in different environments. If you have one class that needs one extra behaviour and that combination will never change, a regular method on the class, or an inheritance one-shot, is fine. Wrapping for the sake of wrapping is overhead.
The other failure mode is too many decorators. A 7-layer chain (auth, rate limiting, caching, retry, logging, metrics, encryption, then the leaf) is technically composable but is a debugging nightmare. Each layer is one extra method on the stack, one extra place to look when behaviour is wrong. If your chain is consistently this deep, consider whether some decorators belong inside the leaf (a built-in retry on the HTTP client itself) or whether they belong in a different layer entirely (rate limiting at the load balancer, not in-process).
Finally, Decorator is the wrong pattern when the augmentation needs the wrapped object's internal state. A decorator can only see the wrapped object through the public interface; if the new behaviour requires reaching into private fields or overriding a non-public method, you are looking at inheritance territory or, more commonly, at a design that wants to be reorganised. Decorator wraps; it does not subclass.
Three flavours of question recur. The first is design: "design a notification system with retries, logging, and rate limiting" or "design an HTTP client middleware chain". The expected first move is to define one base interface (Notifier, HttpClient), implement one or two leaves, and write each cross-cutting concern as its own decorator class that takes the same interface in its constructor. The follow-up is usually about adding a fourth concern; that is your cue to say "new class implementing the interface, wrap it into the chain at wiring time, no other code changes".
The second is comparison: "Decorator vs. inheritance", "Decorator vs. Adapter", "Decorator vs. Proxy". The inheritance answer is about combinatorial explosion (2^N subclasses vs. N decorator classes) and runtime composability (decorators chosen at wiring time, inheritance at compile time). The Adapter answer is about whether the interface changes: Decorator keeps it, Adapter changes it. The Proxy answer is about intent: Proxy controls access (lazy, remote, security); Decorator augments behaviour (log, retry, cache). The fuzzy middle is fine to call out; modern Spring uses both terms loosely for AOP-generated wrappers.
The third is implementation depth: "how do you avoid stack-trace pollution" and "how does Spring make this work without writing each decorator class by hand". The first is honest: one stack frame per layer is the cost of the pattern, and deep chains are the reason to keep decorator counts modest. The second is the AOP answer: Spring uses byte-code generation (CGLIB) or JDK dynamic proxies to generate decorator classes at runtime when you put @Transactional, @Cacheable, or @Retryable on a method. The annotations are the spec; the proxy is the auto-generated decorator. Mention this and you have signalled that you understand the bridge between hand-written Decorator (what you would code on a whiteboard) and AOP (what Spring does in production).
The companion exercise gives you a Notifier interface, a concrete EmailNotifier (already implemented), and stubs for LoggingNotifier and RetryingNotifier. You implement the two decorators so that LoggingNotifier prints the recipient and message before delegating to the wrapped notifier, and RetryingNotifier retries send up to N attempts, succeeding silently on the first one that works and re-throwing the last exception if all attempts fail. The validator checks each decorator in isolation, asserts both implement Notifier, and verifies that stacking them (Retrying(Logging(Email))) delivers the message end-to-end.
Open the exercise: Decorator: Wrap a Notifier with Logging and Retries.