A component often starts with one clear responsibility and then attracts optional behavior. A client sends a request; later the system also needs logging. Some deployments need metrics. Others need caching or access checks. Putting every option inside the original component can make its core job harder to see, while creating subclasses for every combination quickly becomes awkward.

The Decorator pattern offers another shape. A decorator implements the same contract as the component it wraps, performs additional work, and delegates the main operation to that wrapped component. Because callers still see the same contract, decorators can be added, removed, and combined without teaching callers about each feature.

This article explains the mental model, builds the smallest useful example, shows why decorator order matters, and identifies cases where a simpler design is better.

Think of a decorator as a transparent wrapper

Suppose an application reads documents through this interface:

interface DocumentStore:
    function load(id): Document

The basic implementation reads from persistent storage:

class DatabaseDocumentStore implements DocumentStore:
    function load(id):
        return database.findDocument(id)

Now the application needs timing information for document loads. We could edit DatabaseDocumentStore, but that couples measurement to database access. It also means another store implementation must add the same measurement logic separately.

A decorator wraps any DocumentStore instead:

class TimedDocumentStore implements DocumentStore:
    inner
    clock
    metrics

    function load(id):
        started = clock.now()
        try:
            return inner.load(id)
        finally:
            elapsed = clock.now() - started
            metrics.record("document_load", elapsed)

Construction connects the pieces:

store = TimedDocumentStore(
    DatabaseDocumentStore(database),
    clock,
    metrics
)

Callers still receive a DocumentStore and still call load(id). They do not need a special path for timing.

That substitutability is the key property. A decorator is useful because the wrapper and wrapped object satisfy the same role from the caller’s point of view.

Separate the stable operation from optional policy

The example contains two different concerns:

  • loading a document is the core operation;
  • measuring the operation is an additional policy around it.

A decorator gives the policy its own unit without changing the operation’s public shape.

This is more than moving lines into another class. The dependency direction changes. TimedDocumentStore depends on the DocumentStore contract, not on DatabaseDocumentStore specifically. It can therefore wrap a database store, an in-memory store, or another decorator.

That is what makes composition possible.

Compose behaviors instead of creating combination subclasses

Suppose a cache is useful too. A caching decorator might look like this:

class CachedDocumentStore implements DocumentStore:
    inner
    cache

    function load(id):
        cached = cache.get(id)
        if cached exists:
            return cached

        document = inner.load(id)
        cache.put(id, document)
        return document

Now the application can compose both behaviors:

store = TimedDocumentStore(
    CachedDocumentStore(
        DatabaseDocumentStore(database),
        cache
    ),
    clock,
    metrics
)

No TimedCachedDatabaseDocumentStore subclass is required. If another implementation of DocumentStore appears, the existing decorators can wrap it when their assumptions still hold.

This avoids a common inheritance problem. If independent features are represented only through subclasses, combinations multiply. With three binary features, a subclass-based design can drift toward classes for many feature combinations. Decorators model each independent behavior once and combine instances at construction time.

This does not mean decorators are automatically simpler. They trade a larger class hierarchy for an object graph that must be assembled and understood. The trade is worthwhile when the behaviors are genuinely independent and useful in different combinations.

Order is part of the behavior

Decorators are composable, but composition is not necessarily commutative. Changing the order can change observable behavior.

Compare these two arrangements:

Timed(Cached(Database))
Cached(Timed(Database))

In the first arrangement, timing wraps the cache. A cache hit is measured because every load call passes through the timing decorator before reaching the cache.

In the second arrangement, caching wraps timing. A cache hit returns from CachedDocumentStore without calling the timed inner store, so only cache misses are measured by that timing decorator.

Neither arrangement is universally correct. They answer different questions:

Timed(Cached(Database))  -> How long does the caller's load operation take?
Cached(Timed(Database))  -> How long does the underlying uncached load take?

The construction code therefore contains policy. Review decorator order with the same care as ordinary control flow rather than treating wrappers as interchangeable plumbing.

Define failure semantics deliberately

Additional behavior must also account for failures. The timing example uses finally so it records elapsed time whether the inner load succeeds or fails. That may be appropriate for latency measurement, but a success counter would need different logic.

Caching has another boundary condition: it should normally cache only results that the cache policy considers valid. If inner.load(id) fails, blindly caching the failure could change retry behavior. Some systems intentionally use negative caching for selected “not found” results, but that is a separate policy with an explicit lifetime and error classification.

A decorator should make these questions clear:

Does the extra behavior happen before delegation, after it, or both?
Does it run when the inner operation fails?
Can it change the returned value or error?
Does it keep state across calls?

If those answers are unclear, the wrapper may look transparent while actually changing the component’s contract in surprising ways.

Preserve the contract, not every implementation detail

A decorator should be substitutable for the component at the abstraction level callers rely on. That does not require it to reproduce every incidental implementation detail.

For example, if DocumentStore.load promises to return the current document or report that it cannot be loaded, a cache decorator must preserve that meaningful behavior according to an explicit freshness policy. If callers secretly depend on the database implementation performing a query on every call, however, that dependency is outside the apparent abstraction and a cache will expose it.

This is why decorators often reveal weak contracts. Before wrapping an operation, identify the guarantees that must survive the wrapper: return values, errors, ordering, freshness, side effects, and relevant performance or resource expectations.

A decorator that violates those guarantees is not merely an implementation variation. It changes the abstraction.

Keep decorators narrow

A decorator is easiest to reason about when it adds one coherent policy. A wrapper that performs authorization, caching, retries, logging, metrics, and data transformation has recreated the original mixing problem in another class.

Narrow decorators also make ordering easier to inspect:

Authorized(
    Timed(
        Cached(
            DatabaseStore()
        )
    )
)

Even this object graph can become difficult to understand when it grows. Centralize construction in a clear composition point and give important compositions names when they represent stable application policy. Avoid assembling different orders casually throughout the codebase.

Testing can remain focused as well. Test the database store for storage behavior, the cache decorator with a simple fake inner store for caching behavior, and the timing decorator with controlled dependencies for measurement behavior. A smaller number of integration tests can then verify that the production composition has the intended order.

Know when another technique is simpler

Decorators fit behavior that surrounds or augments an operation while preserving a common role. They are not a general replacement for conditionals or ordinary functions.

A helper function may be clearer when the added behavior is used in one place and does not need substitutability. A configuration option inside a component may be simpler when there is one stable variation with no meaningful independent composition. A strategy is often a better model when the goal is to choose how the core operation itself is performed rather than wrap an existing implementation with before-or-after behavior.

Inheritance can also be adequate for a small, stable specialization when the subtype relationship is genuine and combinations are not expected. The Decorator pattern becomes especially useful when optional behaviors need to vary independently and callers should remain unaware of those combinations.

Avoid common decorator mistakes

One mistake is exposing wrapper-specific methods and then forcing callers to downcast to use them. That defeats the shared contract that makes decoration transparent.

Another is allowing decorator order to emerge accidentally from dependency-injection registration or unrelated configuration. If order changes semantics, make that order explicit and test the behavior that depends on it.

A third is wrapping a very broad interface. Every decorator then has to implement methods it does not meaningfully affect, often by forwarding them mechanically. A smaller interface centered on the operation being decorated usually produces a clearer design.

Finally, do not add decorators merely to avoid editing existing code. If a behavior is fundamental to the component’s contract, putting it in the core implementation may communicate the design better. Decoration is most useful for behavior that is separable, composable, and meaningful at the same abstraction boundary.

Conclusion

The Decorator pattern adds behavior by wrapping an object behind the same contract and delegating the core operation. Its main benefit is not the wrapper syntax; it is the ability to keep independent policies separate while composing them without changing callers or creating a subtype for every combination.

Use it when optional behaviors genuinely vary independently. Make ordering and failure semantics explicit, keep each decorator narrow, and preserve the guarantees callers depend on. When one straightforward implementation already communicates the behavior clearly, keep the simpler design.