👋 Want to know more about me? Follow my .NET discussions and see what other developers are asking me on LinkedIn ↗
Decorator Design Pattern in C#: A Beginner-Friendly Guide with the Coffee Example

Decorator Design Pattern in C#: A Beginner-Friendly Guide with the Coffee Example

If you've ever ordered a coffee from a café, you've already used the Decorator Design Pattern without realizing it.

You begin with a simple coffee. Then you may ask for milk. Next, you add sugar. Perhaps you finish it with whipped cream or chocolate syrup.

Notice something interesting. The coffee itself never changes. Instead, each topping simply adds something extra to the existing coffee.

This is exactly what the Decorator Design Pattern does.


What is the Decorator Pattern?

The Decorator Pattern is a Structural Design Pattern whose purpose is:

Add new functionality to an existing object dynamically without modifying its original class.

Instead of creating dozens of subclasses for every possible combination of features, we simply "wrap" an existing object with another object that adds extra behavior.


The Coffee Shop Analogy

Imagine visiting your favourite coffee shop.

The shop offers a plain coffee for ₹100.

Now you have several choices.

  • Add Milk (+₹20)
  • Add Sugar (+₹10)
  • Add Whipped Cream (+₹30)
  • Add Chocolate Syrup (+₹40)

Instead of creating separate classes like:

  • CoffeeWithMilk
  • CoffeeWithSugar
  • CoffeeWithMilkAndSugar
  • CoffeeWithMilkAndSugarAndCream
  • CoffeeWithMilkAndChocolate
  • CoffeeWithSugarAndCream

...the Decorator Pattern lets us build the coffee one layer at a time.

Simple Coffee

↓

Milk

↓

Sugar

↓

Whipped Cream

Every new ingredient wraps the previous coffee and adds its own behaviour.


Step 1 - Create the Coffee Interface


public interface ICoffee
{
    string GetDescription();
    decimal GetCost();
}

Every coffee in our system must be able to tell us two things:

  • Its description
  • Its total price

Step 2 - Create the Basic Coffee


public class SimpleCoffee : ICoffee
{
    public string GetDescription()
    {
        return "Coffee";
    }

    public decimal GetCost()
    {
        return 100;
    }
}

This is our original object.

It knows absolutely nothing about milk, sugar or cream.


Step 3 - Create the Base Decorator


public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee coffee;

    protected CoffeeDecorator(ICoffee coffee)
    {
        this.coffee = coffee;
    }

    public virtual string GetDescription()
    {
        return coffee.GetDescription();
    }

    public virtual decimal GetCost()
    {
        return coffee.GetCost();
    }
}

Notice that the decorator itself also implements ICoffee.

More importantly, it contains another ICoffee.

This allows decorators to wrap decorators.


Step 4 - Milk Decorator


public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee)
        : base(coffee)
    {
    }

    public override string GetDescription()
    {
        return coffee.GetDescription() + ", Milk";
    }

    public override decimal GetCost()
    {
        return coffee.GetCost() + 20;
    }
}

Milk doesn't replace the coffee.

It simply asks the existing coffee for its price, then adds ₹20.


Step 5 - Sugar Decorator


public class SugarDecorator : CoffeeDecorator
{
    public SugarDecorator(ICoffee coffee)
        : base(coffee)
    {
    }

    public override string GetDescription()
    {
        return coffee.GetDescription() + ", Sugar";
    }

    public override decimal GetCost()
    {
        return coffee.GetCost() + 10;
    }
}

Step 6 - Using the Decorators


ICoffee coffee = new SimpleCoffee();

coffee = new MilkDecorator(coffee);

coffee = new SugarDecorator(coffee);

Console.WriteLine(coffee.GetDescription());

Console.WriteLine(coffee.GetCost());

Output:

Coffee, Milk, Sugar

130

How Does It Actually Work?

Let's understand what happens internally.

Initially:

SimpleCoffee

After adding milk:

MilkDecorator
      │
      â–Ľ
SimpleCoffee

After adding sugar:

SugarDecorator
      │
      â–Ľ
MilkDecorator
      │
      â–Ľ
SimpleCoffee

The outermost object is now SugarDecorator.

When we call:


coffee.GetCost();

The call travels through every layer.

SugarDecorator.GetCost()

↓

MilkDecorator.GetCost()

↓

SimpleCoffee.GetCost()

↓

100

↑

Milk adds 20

↑

Sugar adds 10

↑

130

Each decorator performs its own work before returning the final result.


Why Not Use Inheritance?

Imagine your coffee shop keeps introducing new toppings.

  • Milk
  • Sugar
  • Cream
  • Chocolate
  • Caramel
  • Vanilla

If inheritance were used, you would eventually need classes like:

  • CoffeeWithMilk
  • CoffeeWithSugar
  • CoffeeWithCream
  • CoffeeWithMilkAndSugar
  • CoffeeWithMilkAndCream
  • CoffeeWithSugarAndCream
  • CoffeeWithMilkSugarCreamChocolate

The number of subclasses grows rapidly.

This is known as the class explosion problem.

The Decorator Pattern completely avoids this issue by combining decorators dynamically at runtime.


Advantages of the Decorator Pattern

  • Add new behaviour without modifying existing classes.
  • Follow the Open/Closed Principle.
  • Avoid huge inheritance hierarchies.
  • Mix and match features at runtime.
  • Each decorator has a single responsibility.
  • Very flexible and easy to extend.

Disadvantages

  • Can introduce many small classes.
  • The object chain can become difficult to debug if too many decorators are stacked.
  • Understanding the execution flow takes practice.

Real-World Examples in .NET

The Decorator Pattern appears in many places within the .NET ecosystem.

Stream Classes


FileStream

↓

BufferedStream

↓

StreamReader

Each stream wraps another stream while adding additional functionality.

  • FileStream provides file access.
  • BufferedStream improves performance.
  • StreamReader converts bytes into readable text.

Each layer enhances the previous one without modifying it.


Decorator vs Inheritance

Inheritance Decorator
Behaviour fixed at compile time Behaviour added at runtime
Many subclasses Few reusable decorators
Difficult to extend Easy to extend
Can cause class explosion Avoids class explosion
Less flexible Highly flexible

When Should You Use the Decorator Pattern?

Choose the Decorator Pattern whenever:

  • You need to add optional features to objects.
  • You want to avoid creating many subclasses.
  • Features should be combined dynamically.
  • You want to keep classes focused on a single responsibility.

Conclusion

The Decorator Pattern is one of the most elegant design patterns because it promotes composition over inheritance.

Rather than creating new subclasses every time a new feature is introduced, we simply wrap an existing object with another object that enhances its behaviour.

The coffee example perfectly demonstrates this idea:

Simple Coffee

↓

Milk

↓

Sugar

↓

Whipped Cream

Each decorator adds only one responsibility while keeping the original coffee completely unchanged.

Once you understand this simple example, you'll begin to recognize the Decorator Pattern in many real-world .NET classes such as BufferedStream, StreamReader, and numerous middleware and pipeline-based frameworks.

Class Guidelines for Effective 1-on-1 Learning

To keep every session productive and distraction-free, please follow these simple guidelines:

  • Quiet Environment: Join from a calm, private room with minimal background noise. Avoid public or noisy places.
  • No Interruptions: Inform family/roommates in advance. Keep doors closed during class.
  • Mobile on Silent / DND: Set your phone to Silent or Do Not Disturb to prevent calls and notifications.
  • Be Fully Present: Do not multitask. Avoid attending to other calls, visitors, or errands during the session.
  • Stable Setup: Use a laptop/desktop with a stable internet connection and required software installed (Visual Studio/.NET, SQL Server, etc.).
  • Punctuality: Join on time so we can utilize the full session effectively.
  • Prepared Materials (If any): Keep project files, notes, and questions ready for quicker progress.

Following these guidelines helps you focus better and ensures I can deliver the best learning experience in every class.

Schedule a Quick 10-Minute Call

I prefer to start with a short 10-minute free call so I can understand:

  • Your learning objectives and career goals
  • Your current skill level
  • The exact topics you want to learn

Why? Because course content, teaching pace, and fees all depend on your needs — there’s no “one-size-fits-all” pricing. Please leave your details below, and I’ll get back to you to arrange a convenient time for the call.




Note: Payment is made only after your first class, once you’re completely satisfied. However, fees paid after the first class are non-refundable. This helps maintain scheduling commitments and allows me to reserve your preferred time slot with full attention.

Google Review Testimonials

.NET Online Training
Average Rating: 4.9
Votes: 50
Reviews: 50