Skip to content

Decorator Pattern

The Decorator pattern attaches additional behaviour to an object at runtime by wrapping it in decorator objects. It’s an alternative to subclassing for extending functionality.

  • Adding cross-cutting concerns (logging, caching, validation, auth) without modifying the core class
  • When you need combinations of behaviour that would create an explosion of subclasses
  • Middleware pipelines (HTTP middleware, command pipelines)
// Core interface
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task SaveAsync(Product product);
}
// Real implementation
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _db;
public ProductRepository(AppDbContext db) => _db = db;
public async Task<Product?> GetByIdAsync(int id) =>
await _db.Products.FindAsync(id);
public async Task<IEnumerable<Product>> GetAllAsync() =>
await _db.Products.ToListAsync();
public async Task SaveAsync(Product product)
{
_db.Products.Update(product);
await _db.SaveChangesAsync();
}
}
// Caching decorator
public class CachedProductRepository : IProductRepository
{
private readonly IProductRepository _inner;
private readonly IMemoryCache _cache;
public CachedProductRepository(IProductRepository inner, IMemoryCache cache)
{
_inner = inner;
_cache = cache;
}
public async Task<Product?> GetByIdAsync(int id)
{
var key = $"product:{id}";
if (_cache.TryGetValue(key, out Product? cached)) return cached;
var product = await _inner.GetByIdAsync(id);
if (product != null)
_cache.Set(key, product, TimeSpan.FromMinutes(5));
return product;
}
public Task<IEnumerable<Product>> GetAllAsync() => _inner.GetAllAsync();
public async Task SaveAsync(Product product)
{
await _inner.SaveAsync(product);
_cache.Remove($"product:{product.Id}");
}
}
// Logging decorator
public class LoggingProductRepository : IProductRepository
{
private readonly IProductRepository _inner;
private readonly ILogger<LoggingProductRepository> _logger;
public LoggingProductRepository(IProductRepository inner, ILogger<LoggingProductRepository> logger)
{
_inner = inner;
_logger = logger;
}
public async Task<Product?> GetByIdAsync(int id)
{
_logger.LogInformation("Fetching product {Id}", id);
var result = await _inner.GetByIdAsync(id);
_logger.LogInformation("Product {Id} {Found}", id, result != null ? "found" : "not found");
return result;
}
public Task<IEnumerable<Product>> GetAllAsync() => _inner.GetAllAsync();
public Task SaveAsync(Product product) => _inner.SaveAsync(product);
}
// Compose decorators in DI
services.AddScoped<IProductRepository>(provider =>
{
var db = provider.GetRequiredService<AppDbContext>();
var cache = provider.GetRequiredService<IMemoryCache>();
var logger = provider.GetRequiredService<ILogger<LoggingProductRepository>>();
IProductRepository repo = new ProductRepository(db);
repo = new CachedProductRepository(repo, cache);
repo = new LoggingProductRepository(repo, logger);
return repo;
});
// Higher-order function as decorator
function withLogging<T extends (...args: any[]) => any>(fn: T, name: string): T {
return ((...args: Parameters<T>): ReturnType<T> => {
console.log(`${name} called with`, args);
const result = fn(...args);
console.log(`${name} returned`, result);
return result;
}) as T;
}
function add(a: number, b: number): number {
return a + b;
}
const loggedAdd = withLogging(add, 'add');
loggedAdd(2, 3); // logs: "add called with [2, 3]", "add returned 5"

Express middleware is the Decorator pattern applied to HTTP handlers:

// Each middleware wraps the next one
app.use(cors()); // decorates with CORS headers
app.use(helmet()); // decorates with security headers
app.use(rateLimiter()); // decorates with rate limiting
app.use(authenticate); // decorates with auth check
app.use(router); // the core handler

Python has first-class syntax for decorators:

import functools
import time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timed
def process_report(data):
# ... expensive operation ...
return result
DecoratorInheritance
CompositionRuntimeCompile-time
CombinationsMix and match freelyCombinatorial explosion
Single ResponsibilityEach decorator does one thingSubclasses accumulate concerns
Open/ClosedAdd new decorators without modifying existingChanging base class affects all children