Decorator Pattern: Add Toppings to Your Objects, One Layer at a Time
Learn the Decorator pattern with a simple dosa-toppings story. Wrap objects in layers to add new behaviour at runtime, without making new subclasses.
๐ฅ A plain dosa and a hungry customer
Meet Arjun. He is fourteen, always hungry, and every evening after tuition he stops at the same dosa stall near the bus stop in Bengaluru. The stall belongs to Ravi Anna, who has been making dosas for twenty years. Ravi Anna makes one thing very, very well: a plain dosa. Crispy, golden, simple. It costs 40 rupees.
Today Arjun has pocket money, so he gets ideas. "Anna, add masala."
Watch what Ravi Anna does. He does not throw the dosa away and cook a brand new dish called "masala dosa" from zero. He takes the same plain dosa sitting on the tawa and spreads a layer of potato masala on it. Plain dosa becomes masala dosa. Price goes up by 20 rupees.
Arjun is feeling rich. "Cheese also, Anna."
Ravi Anna takes the masala dosa and grates a cheese layer on top of it. Now it is a cheese masala dosa. Another 30 rupees.
Behind Arjun in the queue stands his cousin Sneha, a second-year CSE student who came along for the snack. She wants butter, masala, and cheese. No problem for Ravi Anna. Same plain dosa at the centre, three layers stacked on top, three small additions to the bill.
Sneha watches the stall the way only a CSE student can, and suddenly grabs Arjun's arm. "Did you see? He never invents a new dish. He has one base item and a few toppings. Each topping wraps the dosa below it. Each topping adds its own taste and its own price. And every layered dosa is still, in the end, a dosa โ you fold it and eat it the same way."
Now she asks the killer question. "What if the menu was written with classes? PlainDosa, MasalaDosa, CheeseDosa, CheeseMasalaDosa, ButterCheeseMasalaDosa..." Ravi Anna would need a new class for every single combination. With 5 toppings, that is 2 ร 2 ร 2 ร 2 ร 2 = 32 classes. With 10 toppings, more than 1000 classes. The poor cook would spend his life writing menu cards instead of making dosas.
Ravi Anna's trick โ keep one base, wrap it in layers, each layer adds something โ is exactly the Decorator pattern. This whole article is that one stall, told slowly.
The journey shows the whole pattern in miniature. The order phase picks the layers. The cook phase stacks them without ever touching the recipe of the base. The pay phase walks down the stack, adding every layer's price.
What is the Decorator pattern?
Decorator is a structural design pattern that lets you attach new behaviour to a single object by wrapping it inside another object called a decorator. The decorator implements the same interface as the object it wraps. Because the wrapper looks exactly like the thing inside it, you can wrap a wrapper again, and again, stacking as many layers as you want.
Each layer does three things:
- It holds a reference to the object inside it (the wrappee).
- It forwards calls to that inner object.
- It adds its own little bit of behaviour before, after, or around that call.
The client at the end holds just one reference. It calls one method. The call ripples down through every layer, and each layer adds its flavour, just like every topping adds taste to the dosa.
Easy way to remember: Decorator = same face, extra muscle. The outside interface never changes. Only the behaviour grows, layer by layer. If the interface changes, it is not a decorator anymore (that would be an Adapter).
The pattern is also called Wrapper, because that is literally what it does โ it wraps.
College corner: Decorator is the textbook proof of the design principle "favour composition over inheritance." Inheritance creates an is-a relationship that is fixed when the class is compiled and applies to every instance of the class forever. Composition creates a has-a relationship that is assembled object by object, while the program runs. The decorator cleverly uses both: it inherits the interface (so it can stand anywhere the component stands โ this is the Liskov Substitution Principle doing real work) and it composes the behaviour (so each object can carry a different stack). The result also satisfies the Open/Closed Principle: the system is open to new toppings (just add a class) but closed to modification (nobody edits PlainDosa ever again).
The problem it solves
Let us see what happens without the pattern. Suppose Sneha writes a notification system for her college app. Version one sends messages by email only:
class EmailNotifier {
send(message: string): void {
console.log(`Email sent: ${message}`);
}
}Soon, parents ask for SMS. Teachers ask for WhatsApp. The principal wants push notifications. So she starts making subclasses:
// The wrong road: one class per combination
class EmailAndSmsNotifier { /* ... */ }
class EmailAndWhatsAppNotifier { /* ... */ }
class EmailAndSmsAndWhatsAppNotifier { /* ... */ }
class SmsAndPushNotifier { /* ... */ }
// ... and it never endsWith n channels, there are 2โฟ possible combinations. Four channels means up to 16 classes. Five means 32. And there is a second, sneakier problem: inheritance is decided at compile time. Once an object is born as an EmailAndSmsNotifier, it can never become "email only" or "email plus push" while the program runs. The choice is frozen forever โ like a dosa that comes out of the kitchen welded to its toppings.
The root cause: inheritance applies to the whole class, not to one object. You cannot tell this particular notifier, "from now on, also send SMS." You need a way to compose behaviours per object, at runtime.
On the left, every new combination needs a new class. On the right, you build any combination by simply choosing which wrappers to stack. Four small classes can give you all sixteen combinations.
How bad does the explosion get as toppings grow? Here is the brutal arithmetic, drawn:
The first line is the subclassing road: 2โฟ classes, rocketing to 1024 with just ten toppings. The second line is the decorator road: one base plus one small class per topping, crawling along at n + 1. This single chart is the strongest argument for the pattern. When someone asks "why not just subclass?", show them the curve.
โ๏ธ How it works, step by step
The pattern has four small parts. Follow these steps and you can build it for any problem:
- Define a Component interface. This is the common shape that both the real object and every wrapper will share. For our dosa stall:
getDescription()andgetCost(). - Write the ConcreteComponent. This is the base object that does the real work โ the plain dosa. Decorators will add to its behaviour.
- Write a base Decorator class. It implements the Component interface and stores a reference to another Component (the wrappee). By default, it just forwards every call inward. It is a transparent pass-through.
- Write ConcreteDecorators. Each one extends the base decorator and overrides a method to add its own behaviour around the forwarded call. Masala adds its name and 20 rupees. Cheese adds its name and 30 rupees.
- Let the client compose. The client makes a plain dosa, then wraps it in whichever decorators it wants, in whichever order. The final object is still typed as a plain
Component.
The magic line is this: a decorator is a Component (so it can stand wherever a component stands) and has a Component (so it can wrap one). That is why decorators stack.
Here is the whole cast of the pattern, mapped onto Ravi Anna's stall:
| Pattern part | At the dosa stall | In the code | Job |
|---|---|---|---|
| Component | "A dosa, any dosa" | Dosa interface | The common shape everyone agrees on |
| ConcreteComponent | The plain dosa on the tawa | PlainDosa | Does the real base work |
| Base Decorator | "A topping, any topping" | ToppingDecorator | Holds the wrappee, forwards by default |
| ConcreteDecorator | Masala, cheese, butter | MasalaTopping etc. | Adds one taste and one price |
| Client | Arjun and Sneha ordering | The calling code | Chooses which layers to stack |
Read the diagram from top to bottom. PlainDosa and ToppingDecorator both promise to behave like a Dosa. But the decorator also holds a Dosa inside it. So a CheeseTopping can hold a MasalaTopping, which holds a PlainDosa. Three layers, one interface.
It also helps to see the life of one order as states. An order starts as a plain base, gets wrapped any number of times, and is finally served when the bill is totalled:
The loop on Wrapped is the heart of the pattern โ you can stay in that state as long as you like, adding layer after layer, because every wrap produces another valid Dosa.
Real-life code example
Let us code the full dosa stall in TypeScript. Watch how the wrappers stack.
// Step 1: The Component interface.
// Every dosa โ plain or loaded with toppings โ must answer
// these two questions: what are you, and what do you cost?
interface Dosa {
getDescription(): string;
getCost(): number;
}
// Step 2: The ConcreteComponent โ the base item.
class PlainDosa implements Dosa {
getDescription(): string {
return "Plain Dosa";
}
getCost(): number {
return 40; // rupees
}
}
// Step 3: The base Decorator.
// It IS a Dosa (implements the interface)
// and it HAS a Dosa (holds the wrappee).
abstract class ToppingDecorator implements Dosa {
constructor(protected wrappee: Dosa) {}
// By default, just pass the call inward.
getDescription(): string {
return this.wrappee.getDescription();
}
getCost(): number {
return this.wrappee.getCost();
}
}
// Step 4: Concrete decorators. Each adds one topping.
class MasalaTopping extends ToppingDecorator {
getDescription(): string {
return this.wrappee.getDescription() + " + Masala";
}
getCost(): number {
return this.wrappee.getCost() + 20;
}
}
class CheeseTopping extends ToppingDecorator {
getDescription(): string {
return this.wrappee.getDescription() + " + Cheese";
}
getCost(): number {
return this.wrappee.getCost() + 30;
}
}
class ButterTopping extends ToppingDecorator {
getDescription(): string {
return this.wrappee.getDescription() + " + Butter";
}
getCost(): number {
return this.wrappee.getCost() + 15;
}
}
// Step 5: The client composes the order โ STACKING wrappers.
// Order 1: just a plain dosa.
let order1: Dosa = new PlainDosa();
console.log(`${order1.getDescription()} = Rs.${order1.getCost()}`);
// Order 2: masala dosa. Wrap once.
let order2: Dosa = new MasalaTopping(new PlainDosa());
console.log(`${order2.getDescription()} = Rs.${order2.getCost()}`);
// Order 3: Sneha's butter cheese masala dosa. Wrap three times!
// Read it from inside out: PlainDosa -> Masala -> Cheese -> Butter.
let order3: Dosa = new ButterTopping(
new CheeseTopping(
new MasalaTopping(
new PlainDosa()
)
)
);
console.log(`${order3.getDescription()} = Rs.${order3.getCost()}`);
// You can even add a topping AFTER the order is made.
// Arjun leans over: "One more cheese, please!"
order3 = new CheeseTopping(order3);
console.log(`${order3.getDescription()} = Rs.${order3.getCost()}`);Output:
Plain Dosa = Rs.40
Plain Dosa + Masala = Rs.60
Plain Dosa + Masala + Cheese + Butter = Rs.105
Plain Dosa + Masala + Cheese + Butter + Cheese = Rs.135Look at the last two lines carefully. We added a second cheese layer to an already finished order, at runtime, with one line of code. No new class. No editing of old classes. Each getCost() call travels down through every layer โ butter asks cheese, cheese asks masala, masala asks the plain dosa โ and the answer travels back up, growing at every step. That ripple through the layers is the heartbeat of the Decorator pattern.
Here is that ripple, frozen as a sequence diagram. One innocent call from Sneha, four hops down, four answers back up:
Also notice: every order variable is typed as plain Dosa. The client never knows, and never needs to know, how many layers are inside.
And where does Sneha's 105 rupees actually go? Mostly to the base, with each layer taking its small cut:
This pie is a nice mental model for any decorated object: the base contributes the core behaviour, and every wrapper contributes one well-defined slice. If a slice is wrong, you know exactly which class to open.
The same idea in C#
The shape is identical in C#. Here is a compact version with a different flavour โ adding logging and emoji to a simple message sender:
// Component
public interface INotifier
{
void Send(string message);
}
// ConcreteComponent โ the base behaviour
public class EmailNotifier : INotifier
{
public void Send(string message)
=> Console.WriteLine($"Email: {message}");
}
// Base decorator: is-a INotifier, has-a INotifier
public abstract class NotifierDecorator : INotifier
{
protected readonly INotifier Wrappee;
protected NotifierDecorator(INotifier wrappee) => Wrappee = wrappee;
public virtual void Send(string message) => Wrappee.Send(message);
}
// Concrete decorators
public class SmsDecorator : NotifierDecorator
{
public SmsDecorator(INotifier w) : base(w) { }
public override void Send(string message)
{
base.Send(message); // pass inward first
Console.WriteLine($"SMS: {message}"); // then add behaviour
}
}
public class WhatsAppDecorator : NotifierDecorator
{
public WhatsAppDecorator(INotifier w) : base(w) { }
public override void Send(string message)
{
base.Send(message);
Console.WriteLine($"WhatsApp: {message}");
}
}
// Client: stack the layers
INotifier notifier = new WhatsAppDecorator(
new SmsDecorator(
new EmailNotifier()));
notifier.Send("PTM on Friday at 10 AM");
// Output:
// Email: PTM on Friday at 10 AM
// SMS: PTM on Friday at 10 AM
// WhatsApp: PTM on Friday at 10 AMOne call to Send, three channels fire. To change channels, you change which wrappers you stack โ not a single class is edited.
A Python flavour, and the question of order
Python makes decorators feel almost weightless, because functions are objects too. Here is the dosa stall again, plus a demonstration of the most important practical rule โ order matters:
class PlainDosa:
def description(self): return "Plain Dosa"
def cost(self): return 40
class Topping:
"""Base decorator: holds the wrappee, forwards by default."""
def __init__(self, wrappee): self.wrappee = wrappee
def description(self): return self.wrappee.description()
def cost(self): return self.wrappee.cost()
class Masala(Topping):
def description(self): return self.wrappee.description() + " + Masala"
def cost(self): return self.wrappee.cost() + 20
class SchezwanSauce(Topping):
"""A spicy wrapper that DOUBLES the price of whatever it covers."""
def description(self): return self.wrappee.description() + " + Schezwan"
def cost(self): return self.wrappee.cost() * 2
# Order A: masala first, then schezwan doubles (40 + 20) = 120
order_a = SchezwanSauce(Masala(PlainDosa()))
# Order B: schezwan doubles the base (40 * 2), then masala adds 20 = 100
order_b = Masala(SchezwanSauce(PlainDosa()))
print(order_a.description(), "= Rs.", order_a.cost()) # Rs. 120
print(order_b.description(), "= Rs.", order_b.cost()) # Rs. 100Same two toppings, same base, different bills โ only because the stacking order changed. Twenty rupees of difference from one swapped line.
College corner: this ordering effect is not a toy detail; it is the number one production bug with decorators. Compress-then-encrypt shrinks your data nicely, because compression finds patterns in plain text. Encrypt-then-compress saves almost nothing, because good ciphertext looks like random noise with no patterns left to squeeze. Similarly, in a middleware pipeline, putting the authentication wrapper outside the logging wrapper means failed logins never get logged. The general rule: a decorator can only see and transform what the layers inside it have already produced. Treat the stack order as part of your design, write it down, and test at least one property that breaks if someone reorders it.
Where you see it in real software
The Decorator pattern is not just a textbook idea. It quietly runs inside tools you use every day.
- Java I/O streams. This is the most famous example.
new BufferedReader(new InputStreamReader(new FileInputStream("f.txt")))is three decorator layers.FileInputStreamreads raw bytes,InputStreamReaderadds character decoding,BufferedReaderadds buffering and the handyreadLine()method. Each wrapper takes a stream and gives back a richer stream. - .NET stream wrappers. Exactly the same idea: wrap a
FileStreamin aGZipStreamfor compression, then wrap that in aCryptoStreamfor encryption. Each wrapper is still aStream, so you can keep stacking. - Middleware pipelines. Web frameworks like Express (Node.js) and ASP.NET Core pass each request through a chain of middleware โ logging, authentication, compression. Each piece wraps the next handler and adds behaviour around it. It is the decorator idea applied to request handling.
- TypeScript/Java annotations and decorators. TypeScript's
@decoratorsyntax and libraries like NestJS use the same spirit: wrap a class or method with extra behaviour (caching, validation, logging) without editing it. - UI toolkits. Adding a scrollbar or a border around a window component is classically done with decorators โ the wrapped widget is still a widget.
- Open-source reference. The java-design-patterns repository by iluwatar has a clean, runnable decorator example (a troll that becomes a "club-wielding troll" by wrapping) with thousands of stars โ great for extra reading.
๐งญ When to use it and when not to
| Situation | Use Decorator? |
|---|---|
| You need to add behaviour to one object at runtime, not the whole class | โ Yes |
| Features can combine in many ways (toppings, channels, filters) | โ Yes โ wrappers beat 2โฟ subclasses |
| You may need to remove a behaviour later | โ Yes โ unwrap; a subclass can never be "un-subclassed" |
The class is final/sealed and cannot be extended | โ Yes โ wrap it instead |
| There is only one fixed combination, decided once, forever | โ No โ a simple subclass or plain method is easier |
| You need to change the object's interface, not its behaviour | โ No โ that is the Adapter pattern |
| The added behaviour must see or change the object's private internals | โ No โ decorators only see the public interface |
Object identity matters (code uses === or type checks on the concrete class) | โ Be careful โ the wrapper is a different object |
The same decision, drawn as a map. Find where your problem sits:
The top-right quadrant โ many combinations, chosen while the program runs โ is decorator country. The bottom-left โ one combination, known forever โ needs nothing fancier than a subclass or even a plain function.
Common mistakes students make
Mistake 1: Forgetting to forward the call. A decorator must call the wrappee. If your getCost() returns only the topping price and forgets this.wrappee.getCost(), the whole inner stack goes silent. Every decorator method should normally delegate inward.
Mistake 2: Ignoring the order of layers. Encrypt-then-compress and compress-then-encrypt give different results (compressing encrypted data barely shrinks it). The stack order is part of your program's logic โ write it down.
Mistake 3: Breaking the interface. If your wrapper adds a new public method that the interface does not have, clients holding the Component type cannot see it. The moment callers must know the concrete wrapper type, you have lost the pattern's main benefit.
Mistake 4: Using instanceof on decorated objects. order3 instanceof PlainDosa is false even though there is a plain dosa deep inside. Decorated objects fail concrete-type checks. Always program against the interface.
Mistake 5: Death by a thousand layers. Ten tiny decorators, each adding half a line of behaviour, make stack traces ten frames deeper and debugging painful. If many layers always travel together, merge them or build them from a configuration list.
Compare with cousins
Decorator has two look-alike cousins: Proxy and Adapter. All three wrap an object. The difference is why they wrap.
| Question | Decorator | Proxy | Adapter |
|---|---|---|---|
| Interface to the client | Same as the wrapped object | Same as the wrapped object | Different (converted) |
| Main purpose | Add behaviour | Control access (lazy load, permissions, cache) | Change the shape so incompatible code can connect |
| Who builds the wrapper? | The client, on purpose | Usually the framework/wiring; client may not know | The integration code |
| Stacked many layers deep? | Yes, that is the point | Rarely โ usually one | Rarely |
| One-line memory hook | Extra muscle, same face | Gatekeeper, same face | New face, same muscle |
Also worth one line each:
- Composite holds many children and treats them uniformly; Decorator holds exactly one child and adds behaviour. They often work together in UI trees.
- Strategy changes the insides of an object by plugging in a different algorithm; Decorator changes the skin by wrapping around it.
- Chain of Responsibility also passes calls along linked objects, but a handler may stop the chain; in a decorator stack, every layer is expected to forward.
Everything in this article, hung on one tree:
Quick revision box
+--------------------------------------------------------------+
| DECORATOR โ QUICK REVISION |
+--------------------------------------------------------------+
| Idea : Wrap an object to add behaviour. Same interface. |
| Nickname : Wrapper |
| Analogy : Plain dosa + masala + cheese + butter layers |
| Key trick : Decorator IS-A Component and HAS-A Component |
| -> so wrappers can stack on wrappers |
| Parts : Component, ConcreteComponent, |
| BaseDecorator, ConcreteDecorators |
| Wins : No 2^n subclass explosion; add/remove at runtime |
| Watch out : Layer ORDER matters; always forward the call; |
| instanceof checks break on wrapped objects |
| Cousins : Proxy (controls access), Adapter (new interface) |
| Real life : Java BufferedReader, .NET GZipStream/CryptoStream|
| Express / ASP.NET middleware pipelines |
+--------------------------------------------------------------+Practice exercise ๐
Try these on your own. Use the dosa example as your template.
- Chai stall. Build a
Beverageinterface withgetCost()andgetLabel(). CreatePlainChai(Rs. 10) as the base, then decoratorsGinger(+5),Elaichi(+7), andExtraStrong(+5). Make a "double ginger elaichi chai" by stackingGingertwice. Print the label and total. - Text pipeline. Create a
TextSourceinterface withread(): string. Base class returns a fixed sentence. Write three decorators:UpperCaseDecorator,TrimDecorator, andStarBorderDecorator(adds***before and after). Stack them in two different orders and print both results. Explain in one sentence why the outputs differ. - Challenge โ unwrap. Our pattern makes removing a middle layer hard. Write a small function that rebuilds a decorator stack without one given topping (hint: keep the toppings in an array, then fold/reduce the array into a stack). This teaches you why many real systems configure decorators from a list.
- College challenge โ measure the explosion. Write a tiny script that prints, for n = 1 to 12, the number of classes needed by the subclassing road (2โฟ) versus the decorator road (n + 1). Then write two sentences on which SOLID principles the decorator road satisfies and why.
Frequently asked questions
- What is the Decorator pattern in simple words?
- Decorator is a way to add new behaviour to one object by wrapping it inside another object. The wrapper has the same interface, so the client cannot tell the difference. You can stack many wrappers, like toppings on a dosa.
- How is Decorator different from inheritance?
- Inheritance fixes behaviour at compile time for the whole class. Decorator adds behaviour at runtime to one single object. You can also remove a decorator later, but you can never remove a parent class.
- How is Decorator different from Proxy?
- Both wrap an object with the same interface. Decorator adds extra behaviour and is meant to be stacked by the client. Proxy controls access (lazy loading, permissions, caching) and usually manages the object for the client.
- Does the order of decorators matter?
- Yes, very much. Compress-then-encrypt gives a different result than encrypt-then-compress. Always think about the order in which your wrappers run.
- Where is Decorator used in real software?
- Java I/O classes like BufferedReader, .NET stream wrappers like GZipStream and CryptoStream, and middleware pipelines in web frameworks like Express all use the decorator idea.
Further reading
Related Lessons
Proxy Pattern: The Watchman Who Checks Before You Meet the Owner
Learn the Proxy pattern with a society watchman story. Put a stand-in with the same interface in front of a real object to control access to it.
Adapter Pattern: The Travel Plug Trick That Makes Old Code Fit New Code
Learn the Adapter pattern with a simple 3-pin plug and 2-pin socket story. Make old code work with new code without changing either side. Easy examples.
Composite Pattern: Boxes Inside Boxes โ Treat One Thing and Many Things Alike
Learn the Composite pattern with boxes inside a courier parcel. Treat one item and a full group the same way, and add totals easily with simple recursion.
Facade Pattern: One Travel Agent for Your Whole Messy Trip
Learn the Facade pattern with a travel agent story. Hide many complicated subsystems behind one simple method so client code stays short and clean.