DEV Community

Cover image for Design Patterns Simplified: Part 4 – Decorator Pattern (a.k.a. “Wrap It Before You Log It”)
Prateek Prabhakar
Prateek Prabhakar

Posted on • Edited on • Originally published at Medium

Design Patterns Simplified: Part 4 – Decorator Pattern (a.k.a. “Wrap It Before You Log It”)

Decorator Pattern belongs to the Structural category of design patterns.
Why? Because it helps structure your code so you can add new behaviors to existing objects — without modifying their actual code.

Lets understand with a real world analogy.
Your Custom Coffee Order

Need plain espresso? – Here you go!
Want whipped cream? – No problem.
Feeling fancy? Add chocochips, caramel, and a swirl of cream!

Each topping wraps your coffee, adding more features and that too without altering the base espresso. And that basically is the Decorator Pattern.


A Very Sturdy Application: Logging + Retry + Metrics

Let’s say you’ve built a simple service that sends emails.
You start with this,

Class EmailService
    Method Send(to, message)
        Print("Email sent to " + to)
Enter fullscreen mode Exit fullscreen mode

Now the stakeholders want,

  • Logging before the email is sent
  • Metrics on how long it took
  • Retry mechanism if it fails

So, what do you do? Modify Send() and shove everything inside?
NO! That bloats your code and breaks the Single Responsibility Principle.

Here comes the Decorator Pattern to our rescue.


What is the Decorator Pattern?

It is a design pattern that lets you add new functionality to objects at runtime by wrapping them inside decorator classes.

Each decorator adds one feature, like layering different toppings on a pizza. The core logic stays untouched.


Getting back to the very sturdy email service you were building, of course with the changes that were required,

What does the code look like?

//Step 1: Define the base interface
Interface IEmailService
    Method Send(to, message)

//Step 2: Basic implementation
Class BasicEmailService Implements IEmailService
    Method Send(to, message)
        Print("Email sent to " + to)

//Step 3: Create decorators
//Each decorator wraps an existing implementation 
//of IEmailService and adds something extra.

Class LoggingDecorator Implements IEmailService
    Constructor(service)
        this.service = service

    Method Send(to, message)
        Print("Logging: Sending email to " + to)
        service.Send(to, message)

Class MetricsDecorator Implements IEmailService
    Constructor(service)
        this.service = service

    Method Send(to, message)
        StartTimer()
        service.Send(to, message)
        Print("Metric: Email send duration: " + EndTimer())

Class RetryDecorator Implements IEmailService
    Constructor(service)
        this.service = service

    Method Send(to, message)
        For i = 1 to 3
            Try
                service.Send(to, message)
                Break
            Catch Exception
                Print("Attempt " + i + " failed. Retrying...")

//caller logic
emailService = new BasicEmailService()
emailService = new LoggingDecorator(emailService)
emailService = new MetricsDecorator(emailService)
emailService = new RetryDecorator(emailService)

emailService.Send("user@example.com", "Welcome to Decorator Pattern!")
Enter fullscreen mode Exit fullscreen mode

And if you are wondering how does the code flows across the different decorators? Here is how,

RetryDecorator.Send()
    → MetricsDecorator.Send()
        → LoggingDecorator.Send()
            → BasicEmailService.Send()
Enter fullscreen mode Exit fullscreen mode

Each layer focuses on one job and forwards the actual sending to the next layer inside.

So when you call, emailService.Send(...) the execution follows these 8 steps,

  1. This call enters the outermost wrapper first, which is the RetryDecorator.
  2. Inside RetryDecorator.Send(), it tries to send the email (with retries), but it calls service.Send(to, message), where service is actually the MetricsDecorator.
  3. MetricsDecorator.Send() starts a timer, and then calls its own service.Send(to, message) where service is the LoggingDecorator.
  4. LoggingDecorator.Send() logs the attempt and calls its own wrapped service, which is finally the BasicEmailService.
  5. BasicEmailService.Send() actually sends the email. After the email is sent, control returns back up the chain,
  6. LoggingDecorator completes (nothing more to do after the log).
  7. MetricsDecorator stops the timer and logs the time taken.
  8. RetryDecorator exits its loop if successful (or retries if failure occurred earlier).

What did we achieve?

  • No changes to BasicEmailService
  • Each decorator does only one job
  • Decorators can be stacked in any order
  • Add/Remove behavior at runtime
  • Each decorator is testable in isolation

When should you use the Decorator Pattern?

  • To enhance an object’s behavior without subclassing
  • Follow Single Responsibility Principle
  • Want flexibility at runtime
  • Have optional features (e.g., logging, caching, retrying)

Use Cases?

  • Middleware in web apps (e.g., Express.js, ASP.NET Core pipeline)
  • File I/O (add compression, encryption to streams)
  • Logging and monitoring wrappers
  • Decorating UI elements (borders, scrollbars, etc.)

To summarize, it would be apt to say,

Decorator Pattern helps you wrap existing logic with new behavior without changing the original code.
It’s all about flexible, layered, and testable enhancements to your code.

Hope this helped you wrap your head around the Decorator Pattern.

Next up in the series: Adapter Pattern. Let's learn about it.

Top comments (0)