Design Patterns, Explained Simply · Structural Patterns: Fitting Objects Together

Decorator

Add behaviour to an object, such as logging, caching or retries, by wrapping it in objects with the same interface. Stack the wrappers in any combination without subclassing.

The problem

A service fetches data over HTTP. Then you need logging. Then caching for some callers but not others. Then automatic retries, because the remote API fails occasionally. With subclasses you get LoggingFetcher, CachingFetcher, CachingLoggingFetcher, RetryingCachingLoggingFetcher... one class per combination, and the order of features (cache before retry? after?) is frozen into class names.

Real-world analogy

Getting dressed for cold weather: a shirt, then a sweater, then a raincoat. Each layer wraps the previous one and adds something (warmth, waterproofing), and you can choose which layers to wear today. You are still "a person wearing clothes" from the outside, however many layers there are.

Without the pattern

class Fetcher: ...
class LoggingFetcher(Fetcher): ...
class CachingFetcher(Fetcher): ...
class LoggingCachingFetcher(CachingFetcher): ...          # one class per combination
class RetryingLoggingCachingFetcher(LoggingCachingFetcher): ...

The pattern

  • Component interface: what clients call (fetch(url)).
  • Concrete component: the real work (HttpFetcher).
  • Decorators: implement the same interface, hold a wrapped component, and add behaviour before or after delegating to it (LoggingFetcher, CachingFetcher, RetryFetcher).
  • Because a decorator is a component, decorators can wrap other decorators: you compose features by stacking wrappers, and the order is a runtime choice.

The code

FlakyHttp fails the first call, to show the retry decorator at work. The stack is: logging → cache → retry → the real fetcher.

Stacking logging, caching and retries around a fetcherTry it yourself
class FlakyHttp:  # concrete component: fails the first call
    def __init__(self):
        self.calls = 0

    def fetch(self, url):
        self.calls += 1
        if self.calls == 1:
            raise ConnectionError("timeout")
        return f"<data from {url}>"


class RetryFetcher:  # decorator: same interface, wraps another fetcher
    def __init__(self, inner, attempts):
        self.inner, self.attempts = inner, attempts

    def fetch(self, url):
        for attempt in range(1, self.attempts + 1):
            try:
                return self.inner.fetch(url)
            except ConnectionError:
                print(f"  retry: attempt {attempt} failed")
        raise ConnectionError("gave up")


class CachingFetcher:
    def __init__(self, inner):
        self.inner, self.cache = inner, {}

    def fetch(self, url):
        if url not in self.cache:
            self.cache[url] = self.inner.fetch(url)
        else:
            print("  cache: hit")
        return self.cache[url]


class LoggingFetcher:
    def __init__(self, inner):
        self.inner = inner

    def fetch(self, url):
        print(f"log: GET {url}")
        return self.inner.fetch(url)


http = FlakyHttp()
fetcher = LoggingFetcher(CachingFetcher(RetryFetcher(http, attempts=3)))
print(fetcher.fetch("/users/1"))
print(fetcher.fetch("/users/1"))
print("real HTTP calls:", http.calls)

Output

log: GET /users/1
  retry: attempt 1 failed
<data from /users/1>
log: GET /users/1
  cache: hit
<data from /users/1>
real HTTP calls: 2

The first call was logged, retried once and cached; the second was served from the cache. Only two real HTTP calls happened. Changing the order (for example, logging inside the cache so cache hits aren't logged) is just a different wrapping order.

When to use it

  • You want to add cross-cutting behaviour (logging, caching, retries, metrics, authorization, compression) to objects without changing their classes.
  • Features must be combined in different ways for different callers, chosen at runtime.
  • Subclassing would multiply classes, or the class is final / from a library.

When not to use it

  • There is only one fixed combination. Putting the behaviour directly in the class may be simpler.
  • Order matters, and many layers are hard to debug. Deep stacks of wrappers make stack traces long and behaviour harder to follow.
  • Code that checks the concrete type (instanceof HttpFetcher) breaks when the object is wrapped.

You've already used it

  • Java I/O: new BufferedReader(new InputStreamReader(new FileInputStream(f))), the textbook decorator stack.
  • Python: function decorators like @functools.lru_cache and @retry wrap a function with extra behaviour, the same idea applied to functions.
  • Middleware: Express, Laravel and ASP.NET middleware wrap a request handler with logging, auth and compression layers.

Key points

  • A decorator wraps an object with the same interface and adds behaviour before or after delegating.
  • Decorators stack in any order, avoiding a subclass for every combination.
  • Ideal for cross-cutting concerns: logging, caching, retries, metrics, auth.
  • Python's @decorator syntax applies the same idea to functions.
  • Further reading: Refactoring Guru: Decorator and Python's functools.

Practice questions

How does the Decorator pattern differ from subclassing for adding behaviour?

Answer

Subclassing fixes a combination of features at compile time and needs a class per combination. Decorators wrap an object with the same interface at runtime, so features stack in any combination and order, and the original class stays unchanged.

Give a classic example of the Decorator pattern in the Java standard library.

Answer

Java I/O streams: new BufferedReader(new InputStreamReader(new FileInputStream(file))). Each layer implements a reader or stream interface and adds buffering, character decoding and so on to the one it wraps.

Is a Python `@decorator` the same as the Decorator pattern?

Answer

It is the same idea applied to functions: a function wraps another and adds behaviour while keeping the same call signature (@lru_cache, @retry). The GoF pattern wraps objects that share an interface. The syntax is different; the intent is the same.

Why does the order of decorators matter?

Answer

Each wrapper runs around the ones inside it. Logging outside a cache logs every call, including cache hits; logging inside it logs only real fetches. Retry outside a cache retries cache misses; retry inside protects only the real call.

Open in the app to mark lessons complete, take notes and track your progress.