Skip to main content
CodeOath
← All posts

Architecture & Patterns110 min total · 26 parts

ACID, SOLID, and Design Patterns: A Complete Software Design Reference

Part 17 of 26 · ~2 min

Structural Patterns: Decorator

Decorator wraps an object inside another object sharing its interface, layering on one extra piece of behavior per wrapper, all assembled at runtime rather than baked into a subclass. The alternative — a dedicated subclass for every combination of extras a caller might want — multiplies out of control fast (nobody wants to be the one maintaining a class named RetryingAuditedSmsNotifier).

public interface IShiftNotifier { void Send(string message); }

public class SmsShiftNotifier : IShiftNotifier
{
    public void Send(string message) { /* pretend this calls a real SMS gateway, and can fail */ }
}

public class RetryingNotifierDecorator : IShiftNotifier
{
    private readonly IShiftNotifier _inner;
    public RetryingNotifierDecorator(IShiftNotifier inner) => _inner = inner;

    public void Send(string message)
    {
        for (int attempt = 1; attempt <= 3; attempt++)
        {
            try { _inner.Send(message); return; }
            catch when (attempt < 3) { /* try again */ }
        }
    }
}

public class AuditLoggingNotifierDecorator : IShiftNotifier
{
    private readonly IShiftNotifier _inner;
    public AuditLoggingNotifierDecorator(IShiftNotifier inner) => _inner = inner;

    public void Send(string message)
    {
        Console.WriteLine($"[audit] sending: {message}");
        _inner.Send(message);
    }
}

// Decorators stack — each layer adds one concern, composed at runtime.
IShiftNotifier notifier = new AuditLoggingNotifierDecorator(new RetryingNotifierDecorator(new SmsShiftNotifier()));
notifier.Send("Reminder: your shift starts at 9am.");

None of the callers above ever had to be told how many decorators were stacked up, and that's the entire payoff — a shared interface means Send is Send, no matter how many concerns got layered underneath it before the message actually left the building. The same trick, structurally, is what's running every time an HTTP request passes through an ASP.NET Core middleware pipeline, every time a Python function gets wrapped with @decorator syntax, or every time a Java stream gets handed off through a chain of wrapper classes.

One thing worth being deliberate about, because it's easy to assume order doesn't matter: it does, and the two orderings above genuinely behave differently. AuditLoggingNotifierDecorator wrapping RetryingNotifierDecorator — the arrangement above — logs exactly one audit line per logical message, with whatever the retry loop eventually settled on. Flip the wrapping so retry is on the outside instead, and the audit log records every individual attempt, failed ones included — which is what you want if the audit trail exists to answer "did this actually get delivered, and how many tries did it take," and actively wrong if it exists as a simple "we sent this" record. Decide which question the log is answering before picking the order.