Free Interactive Course · Design Patterns

Decorator Design Pattern

Add behaviour by wrapping, not by subclassing — and stack it in any combination you like.

Structural Patternsmediuma.k.a. Wrapper
ShareXLinkedIn
In one sentence

Attach additional responsibilities to an object dynamically, keeping the same interface, without subclassing for every combination.

01

The problem Decorator solves

The problem

You have an HttpClient. Someone asks for request logging, so you write LoggingHttpClient extends HttpClient. Then retries: RetryingHttpClient. Then latency metrics, then response caching.

Now the awkward question arrives: a team needs retries and logging. So you write RetryingLoggingHttpClient. Then someone needs logging with metrics. Then all four, in a particular order, because retries have to happen inside the timing or the numbers are wrong.

Four features means fifteen possible combinations. You cannot subclass your way out of this — the class count grows exponentially while the actual behaviour you're adding is four small, independent things.

02

How the Decorator pattern works

Stop inheriting and start wrapping. A decorator is the thing it decorates, and also holds one:

  1. The decorator implements the same interface as what it wraps, so nothing downstream can tell the difference.
  2. It holds a reference to an inner instance of that interface — which may itself be another decorator.
  3. Each method does its own bit, then delegates to the inner object. Retry calls it several times; logging calls it once and writes a line either side.
Order is behaviour, not preference. Retry(Timing(client)) times each individual attempt. Timing(Retry(client)) times the whole operation including all retries. Both compile, both run, and they answer different questions — which is why a wrapping bug is so much harder to spot than a compile error. Decide what each layer should observe, then stack accordingly.
«interface» HttpClient+ send(request) : ResponseLoggingClientRetryingClientRealHttpClientwrapswrapsnew LoggingClient(new RetryingClient(new RealHttpClient()))every box satisfies the same interface — callers cannot tell how deep the stack is
Participants. The Component interface is what everyone implements. The Concrete Component does the real work. Each Decorator implements the interface and holds one. Because the outermost object is still just an HttpClient, the calling code has no idea it's talking to a stack of four.
03

See it: stack the wrappers

Add wrappers and watch the object nest. The expression at the bottom is the code you would write — and notice that the innermost RealHttpClient never changes, no matter how many layers you pile on.

▶ Try it — wrap and unwrap

An interactive wrapper stack: each decorator you add nests around the real HTTP client, producing an expression such as Logging(Retry(RealHttpClient())), and every combination satisfies the same HttpClient interface.

Add Retry and Timing, then remove and re-add one so the order flips. Timing(Retry(…)) measures the whole operation including retries; Retry(Timing(…)) measures each attempt separately. Your dashboards will show very different p99s depending on which you shipped.
04

Decorator pattern code examples

Retry and logging as independent, stackable wrappers.

public interface HttpClient {
    Response send(Request request);
}

public class RealHttpClient implements HttpClient {
    public Response send(Request r) { /* opens the socket */ }
}

public class RetryingClient implements HttpClient {
    private final HttpClient inner;      // the decorated object
    private final int attempts;

    public RetryingClient(HttpClient inner, int attempts) {
        this.inner = inner;
        this.attempts = attempts;
    }

    @Override public Response send(Request r) {
        RuntimeException last = null;
        for (int i = 0; i < attempts; i++) {
            try {
                return inner.send(r);        // delegate
            } catch (TransientException e) {
                last = e;
                sleep(backoff(i));
            }
        }
        throw last;
    }
}

public class LoggingClient implements HttpClient {
    private final HttpClient inner;
    public LoggingClient(HttpClient inner) { this.inner = inner; }

    @Override public Response send(Request r) {
        log.info("→ {} {}", r.method(), r.url());
        Response resp = inner.send(r);
        log.info("← {} in {}ms", resp.status(), resp.elapsedMillis());
        return resp;
    }
}

// Compose at the wiring site, in whatever order the behaviour requires:
HttpClient client =
    new LoggingClient(
        new RetryingClient(
            new RealHttpClient(), 3));

// Logging sees ONE line per logical call; retries happen inside it, unseen.
// Swap the two and you get a log line per attempt instead.
Read across the tabs: Go's version is the shortest because its interfaces are small and implicit — which is exactly why Go middleware is written this way as a matter of course. C# has the pattern productised as DelegatingHandler. And two languages have a naming collision worth keeping straight: Python's @decorator and TypeScript's @decorator wrap functions and classes at definition time, whereas this pattern wraps objects at runtime — which is what lets two instances in the same process have different layers.
05

How to implement Decorator

  1. Make sure there is a real interface — decorators wrap an abstraction, so a concrete class with no interface can't be decorated.
  2. Give the decorator a constructor taking the inner instance, and store it as the same interface type so decorators can wrap each other.
  3. Implement every method: do your bit, then delegate. A method you forget to forward silently changes behaviour.
  4. Return the interface from your factory functions, not the concrete decorator, so nesting order stays free.
  5. Decide the stacking order deliberately and write down why — Timing(Retry(x)) and Retry(Timing(x)) measure different things.
  6. Keep each decorator to one concern. A wrapper that both retries and caches is just a subclass with extra steps.
06

When to use Decorator — and when not to

Use it when responsibilities need to be added and removed independently, when the combinations would cause a subclass explosion, when you must not (or cannot) modify the original class, or when different callers need different layers around the same underlying object.

Where it goes wrong

Stack traces from hell. Five layers means five frames of delegation between the caller and the actual work, on every stack trace and in every debugger step. Keep the stack shallow and name the classes for what they add.

Identity stops working. decorated == original is false, instanceof RealHttpClient is false, and any code that downcasts or compares by reference breaks. If callers need to reach the inner object, that's a design smell worth fixing rather than exposing.

Wide interfaces make it miserable. Decorating a twenty-method interface means twenty forwarding methods per decorator, and one you forget is a subtle bug. Narrow the interface, or use a language feature that forwards automatically (__getattr__, Proxy).

Order bugs are invisible. Nothing in the type system distinguishes a correct stack from a wrong one, and both run fine — the difference only shows up in your metrics or your logs.

You want to…UseBecause
Add behaviour, keep the same interface, stack freelyDecoratorComposition instead of a combinatorial class hierarchy.
Change the interface to fit a different callerAdapterAdapter converts; Decorator preserves.
Control access, defer creation, or add a remote hopProxySame wrapping shape, but the intent is control rather than enhancement.
Simplify a complicated subsystem behind one entry pointFacadeFacade narrows a big surface; Decorator keeps the surface identical.
Replace behaviour rather than add to itStrategyStrategy swaps one implementation; Decorator layers several.
07

Quick check

🧠 Quick check
You want a latency metric that reflects what your users actually wait for, on a client that retries failed calls three times. Which stack is right?

In the wild

Javanew BufferedReader(new InputStreamReader(new FileInputStream(f))) — the whole java.io package is Decorator, and it's why those constructor calls nest.
C#DelegatingHandler in HttpClientFactory — a named, first-class decorator pipeline for HTTP.
GoHTTP middleware: func(http.Handler) http.Handler. Chi, Gin and the standard library all compose behaviour exactly this way.
Pythongzip.GzipFile(fileobj=open(path,'rb')) — file-like objects wrapping file-like objects, the same shape as java.io.
JavaScriptExpress and Koa middleware, and the Response object in the Fetch API being wrapped by service workers.
C++std::ostream filtering streams, and Boost.Iostreams' filtering_streambuf chains.

Frequently asked questions

What is the difference between the Decorator and Proxy patterns?
Structurally almost nothing — both implement an interface and hold an instance of it. The difference is intent, and it shows up in what the wrapper does with the call. A Decorator always delegates and adds something around it: logging, retries, buffering. A Proxy controls whether and how the call happens at all — lazily creating the real object, checking permissions and refusing, caching so the call never goes out, or turning it into a network request.
Is Python's @decorator the same as the Decorator pattern?
No, they just share a name. Python's @decorator (and TypeScript's) wraps a function or class at definition time — one wrapping, fixed for the whole program. The GoF pattern wraps an object at runtime, so two instances in the same process can have different layers: one HTTP client with retries for a flaky vendor and another without. The syntax can be used to implement the pattern, but it isn't the pattern.
Does the order of decorators matter?
Very much, and it's the most common bug in real use. Timing(Retry(client)) measures the entire operation including all retries; Retry(Timing(client)) records one measurement per attempt. Similarly, caching outside retries never retries a cached hit, while caching inside them may store a response that was about to be retried. Nothing in the type system distinguishes these, so document the intended order where you build the stack.
How do I decorate an interface with twenty methods?
Ideally, don't — a twenty-method interface is the real problem, and narrowing it fixes several things at once. If you can't change it, use whatever forwarding your language offers: Python's __getattr__, a JavaScript Proxy, C#'s DispatchProxy, or Java's java.lang.reflect.Proxy. Otherwise write an abstract base decorator that forwards everything, and have each concrete decorator override only the methods it changes.

Finish the handbook, earn the certificate

All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.

See all 23 patterns →
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.