Skip to content

// Design Patterns · 09

Decorator

Need extra behaviourleads toWrap with same interface

Wrap an object to add behaviour without changing it.

How it fits together

Decorator structure«client»ProductsEndpointGet(id)DatabaseProductRepositoryORM query«interface»ProductRepositoryGetAsync(id)CachedProductRepositoryin-memory cache, 5 mininner on a missinner
uses / callsimplementswraps / holds
  • ProductsEndpoint uses / calls ProductRepository
  • DatabaseProductRepository implements ProductRepository
  • CachedProductRepository implements ProductRepository
  • CachedProductRepository wraps / holds ProductRepository (inner)

Spot it

  • Logging and caching mixed into business logic
  • A subclass per combo: CachedLoggedRepo
  • Cross-cutting code copied into every method

It costs

Wrapper order matters, and stack traces and debugging get deeper.

In code

public interface IProductRepository{    Task<Product?> GetAsync(int id, CancellationToken ct);}​public sealed class CachedProductRepository(IProductRepository inner, IMemoryCache cache)    : IProductRepository{    private static readonly TimeSpan Ttl = TimeSpan.FromMinutes(5);​    public Task<Product?> GetAsync(int id, CancellationToken ct) =>        cache.TryGetValue(id, out Product? hit)            ? Task.FromResult(hit)            : LoadAndCacheAsync(id, ct);​    private async Task<Product?> LoadAndCacheAsync(int id, CancellationToken ct)    {        var product = await inner.GetAsync(id, ct);        if (product is not null)        {            cache.Set(id, product, Ttl);        }​        return product;    }}
Go deeperThe full explanation and what to ask for · 1 min readExpand

A decorator implements the same interface as the object it wraps, holds that inner object, and adds work before or after delegating. Callers cannot tell the difference, so decorators stack.

A decorator that changes what the contract means, such as silently swallowing errors, breaks substitutability. Many DI containers have no built-in decorate step, so registration needs a factory that resolves the inner implementation.

Stream wrappers and HTTP message handler chains are decorators, and web framework middleware is the same idea as a pipeline. Adapter changes the interface; decorator keeps it.

Ask for it

  • decorator
  • wrapper
  • cross-cutting concern
  • inner instance
  • handler chain
  • pipeline

Add caching to ProductRepository without changing it: write a CachedProductRepository decorator with an in-memory cache and a 5-minute expiry that delegates misses to the inner repository. Register it so consumers still ask for ProductRepository.