Skip to content
Sanfor2004
← All articles
Design PatternsDesign PatternsStructural PatternsPythonC++Software Engineering

Decorator (Structural Pattern)

By Ahmed (Sanfor) · 11 Sept 2026 · 5 min read
Successive paper, ribbon, and card layers wrap the same gift.

Start with the design patterns overview · Part 09 of 23 · Structural patterns

Add responsibilities by wrapping an object with components that share its interface. This article follows the example in my 23 Design Patterns repository, connecting the problem, participating classes, Python and C++20 implementations, and the trade-offs that decide whether to use it.

The Problem

A coffee order can add milk once or several times without inventing a drink class for every combination.

Naive Solution

struct CoffeeWithMilk {};
struct CoffeeWithDoubleMilk {}; // another combination

Why It Becomes a Problem

Separate combination classes duplicate base pricing and expand with every add-on.

The Idea

Milk owns a Drink and delegates before adding its own description and price.

Reading the illustration

You can wrap a plain gift with paper, then ribbon, then a card. Each layer adds something without changing the gift inside.

In the repository example, the same design idea addresses this software problem: A coffee order can add milk once or several times without inventing a drink class for every combination.

Structure

Diagram · Python source · C++20 source

Decorator sketch map: Client leads through Milk(Drink) to Coffee or Milk.
Decorator: trace the example from caller through the pattern boundary to its collaborator or result. The arrows show flow, not ownership. Open the full-size diagram.
Client  -->  Milk(Drink)  -->  Coffee or Milk

Participants

Drink is the shared contract. Coffee supplies the base behavior. Milk wraps exactly one Drink. The client owns the outermost wrapper.

Canonical roles in this example:

  • Component — The common contract exposed by leaves, groups, or wrappers. Here: Drink.
  • Concrete Component — The basic implementation before optional wrappers are added. Here: Coffee.
  • Concrete Decorator — A wrapper that keeps the Component contract and adds a specific responsibility. Here: Milk.

Python Example

The complete Python source is shown first.

class Coffee:
    def description(self):
        return "coffee"

    def price_cents(self):
        return 1000


class Milk:
    def __init__(self, inner):
        self.inner = inner

    def description(self):
        return self.inner.description() + " + milk"

    def price_cents(self):
        return self.inner.price_cents() + 200


if __name__ == "__main__":
    drink = Milk(Milk(Coffee()))
    print(f"{drink.description()}: {drink.price_cents()} cents")

Python Output

coffee + milk + milk: 1400 cents

Code Walkthrough

Drink is the shared contract. Coffee supplies the base behavior. Milk wraps exactly one Drink. The client owns the outermost wrapper.

Start at the final call in the Python example. Follow the middle role in the diagram and compare how the C++20 version handles the same responsibility.

C++20 Example

// Monetary amounts in this example are integer cents.
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>

struct Drink {
    virtual ~Drink() = default;
    virtual std::string description() const = 0;
    virtual int price_cents() const = 0;
};
struct Coffee final : Drink {
    std::string description() const override { return "coffee"; }
    int price_cents() const override { return 10; }
};
class Milk final : public Drink {
    std::unique_ptr<Drink> inner_;
public:
    explicit Milk(std::unique_ptr<Drink> inner) : inner_(std::move(inner)) {
        if (!inner_) throw std::invalid_argument("Missing drink");
    }
    std::string description() const override { return inner_->description() + " + milk"; }
    int price_cents() const override { return inner_->price_cents() + 2; }
};
int main() {
    std::unique_ptr<Drink> drink = std::make_unique<Coffee>();
    drink = std::make_unique<Milk>(std::move(drink));
    drink = std::make_unique<Milk>(std::move(drink));
    std::cout << drink->description() << ": " << drink->price_cents() << '\n';
}

C++20 Output

coffee + milk + milk: 14

When to Use

Use it for optional, composable behavior that preserves the wrapped contract.

Use cases

Streams with compression or encryption layers are a technical context; order and failure handling matter.

When NOT to Use

Avoid it when a simple list of ingredients and a sum express the entire requirement; this example intentionally illustrates the structure.

Advantages

Add-ons combine at runtime (The period when a compiled program is executing), and the base implementation (The concrete code that fulfills an interface or performs an operation) stays small.

Trade-offs

Wrapper order can change behavior. Many tiny objects complicate debugging, and preserving the interface does not automatically preserve every semantic promise.

Proxy · Composite

Common Confusion

Proxy controls access to an object. Decorator adds responsibilities. A wrapper’s shape alone does not tell you its intent.

Terms to Remember

  • Decorator — Add behavior by wrapping an object in another object with the same interface.
  • Component — The common contract exposed by leaves, groups, or wrappers. Example: Drink.
  • Concrete Component — The basic implementation before optional wrappers are added. Example: Coffee.
  • Concrete Decorator — A wrapper that keeps the Component contract and adds a specific responsibility. Example: Milk.

Interview Vocabulary

Interview Question

Would logging before encryption observe the same data as logging after encryption?

Mini Challenge

Add a Syrup decorator costing 3, wrap it in two different orders, and explain the resulting descriptions.

Compare the two versions

This is the GoF Object-wrapping Decorator, not Python function-decorator syntax. Python retains the wrapped drink; C++ transfers exclusive Ownership into each wrapper. Prices use integer cents. A list of ingredients is simpler if price addition is the whole problem. The two examples express the same pattern responsibility; compare their setup and output before changing an input.

Check yourself

  1. Why can Milk wrap another Milk without knowing its concrete type?
  2. When would the naive solution on this page be easier to maintain? Give a concrete example.
  3. Change one input in the Python example. Predict the output and explain which responsibility handles the change.

Run and explore the example

The Python code comes from python/main.py, with expected output. The C++20 code comes from structural/decorator/cpp/main.cpp. Follow the repository’s C++20 build instructions to compile it and compare the result with expected.txt. The output demonstrates this example’s behavior; it does not cover every input or the challenge above.

The source example and adapted explanation are © 2026 Sanfor2004, provided under the MIT license. The sketchbook cover is an illustration preserved from this site’s original pattern lessons.

Continue the series

Ahmed (Sanfor)Backend and automation engineer in Alexandria. I build the systems businesses run on, and paint between deploys.

Keep reading

Design PatternsAbstract Factory (Creational Pattern)5 minDesign PatternsAdapter (Structural Pattern)4 minDesign PatternsBridge (Structural Pattern)5 min

Have manual work eating your week?

Book a free call