DEV Community

Cover image for Decorator: The Coffee Add-Ons Pattern
Vignesh Athiappan
Vignesh Athiappan

Posted on

Decorator: The Coffee Add-Ons Pattern

You order a coffee. Base price ₹100. Add milk (+₹20), add caramel (+₹30), add whipped cream (+₹25). Each add-on wraps the coffee and piles on a little more — more cost, more description — but it's still a coffee underneath. You can stack as many as you want, in any order.

The barista does not keep a separate recipe called CoffeeWithMilkAndCaramelAndWhippedCream. That would be madness — every combination would need its own recipe. Instead, each topping is a wrapper that adds something on top of whatever it wraps.

That's the Decorator pattern: add behavior to an object by wrapping it, without changing the original.

The problem: class explosion

Say you have a notification sender, and the requirements pile up:

  • Sometimes you want to log before sending.
  • Sometimes you want to encrypt the message.
  • Sometimes you want to retry on failure.
  • ...and sometimes all three, in different combinations.

If you make a class for every combo — LoggingEncryptingRetryingSender, LoggingRetryingSender, EncryptingSender — you get an explosion. Three features means up to eight classes. Five features means thirty-two. This does not scale.

The Decorator way

Everyone shares one interface — the real sender and every wrapper alike:

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

// The real thing — the plain coffee
public class EmailSender : INotificationSender
{
    public void Send(string message) => Console.WriteLine($"Sending: {message}");
}
Enter fullscreen mode Exit fullscreen mode

Now the key move. Each decorator implements the interface AND holds an instance of that same interface inside it:

public class LoggingDecorator : INotificationSender
{
    private readonly INotificationSender _inner;   // the thing it wraps
    public LoggingDecorator(INotificationSender inner) => _inner = inner;

    public void Send(string message)
    {
        Console.WriteLine("LOG: about to send");   // behavior BEFORE
        _inner.Send(message);                      // call what's inside
        Console.WriteLine("LOG: sent");            // behavior AFTER
    }
}

public class EncryptDecorator : INotificationSender
{
    private readonly INotificationSender _inner;
    public EncryptDecorator(INotificationSender inner) => _inner = inner;

    public void Send(string message)
    {
        var encrypted = $"[encrypted]{message}[/encrypted]";
        _inner.Send(encrypted);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now stack them like toppings

INotificationSender sender =
    new LoggingDecorator(          // outermost wrapper
        new EncryptDecorator(      // middle wrapper
            new EmailSender()));   // the real coffee inside

sender.Send("Hello!");
Enter fullscreen mode Exit fullscreen mode

Output:

LOG: about to send
Sending: [encrypted]Hello![/encrypted]
LOG: sent
Enter fullscreen mode Exit fullscreen mode

Read the nesting from the inside out: plain email, wrap it in encryption, wrap that in logging. Each layer adds its bit and passes the call inward — exactly like espresso, then milk, then caramel.

Want retry too? Write one RetryDecorator and wrap it around. Want logging without encryption? Just don't wrap with encryption. Any combination, zero new combo-classes. That's the whole win.

The trap: this is NOT inheritance

The natural instinct is to reach for inheritance — make a FancyEmailSender : EmailSender that adds logging. But that bakes the behavior in permanently, and the moment you want logging plus encryption, the combo explosion is back.

The difference comes down to one phrase:

  • Inheritance is IS-A"I am a fancier email sender." Locked in at compile time.
  • Decorator is HAS-A"I have an email sender inside me, and I add something around it." Composed at runtime.

This is the famous principle composition over inheritance. A decorator holds another object and wraps it, instead of inheriting from it. That's what lets you nest wrappers like Russian dolls and choose the combination on the fly.

One subtle but important detail: the decorator holds the interface (INotificationSender), not the concrete EmailSender. That's deliberate — it's what lets a decorator wrap another decorator, not just the real sender. Hold the concrete class and the stacking breaks.

You already use this in .NET

Stream stream   = new FileStream("data.txt", FileMode.Open);
Stream buffered = new BufferedStream(stream);   // wrap: add buffering
Stream zipped   = new GZipStream(buffered, CompressionMode.Compress);
Enter fullscreen mode Exit fullscreen mode

A Stream wrapping a Stream wrapping a Stream — pure Decorator. It's also how ASP.NET Core middleware works: each piece wraps the next.

One line to remember

Decorator = coffee add-ons. Wrap the object to pile on behavior, stack any combination at runtime, no combo-classes needed.


Part 5 of a series on must-know design patterns in C#, explained the simple way. Earlier parts covered Singleton, Factory Method, Builder, and Adapter. Next up: Facade — the receptionist pattern.

Top comments (0)