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}");
}
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);
}
}
Now stack them like toppings
INotificationSender sender =
new LoggingDecorator( // outermost wrapper
new EncryptDecorator( // middle wrapper
new EmailSender())); // the real coffee inside
sender.Send("Hello!");
Output:
LOG: about to send
Sending: [encrypted]Hello![/encrypted]
LOG: sent
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);
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)