// Design Patterns · 09
Decorator
Wrap an object to add behaviour without changing it.
How it fits together
- 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 readExpandCollapse
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.