Design Patterns: Structural · Structural Patterns

Decorator Pattern

Wrapping a base object in layers, each adding one behavior, without a subclass for every combination.

  • 5 min read
  • 9 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The pattern
  3. How it works, step by step
  4. Where you'll see it
  5. Choosing among the structural patterns
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

Decorator is the structural pattern for adding behavior to an object without changing its class: you wrap it in another object with the same interface, which does its extra work and passes the call along. Because each wrapper is still a valid object of that interface, wrappers stack - which is how java.io streams, Python's file layers and web middleware are built. This skill covers the pattern, how stacking works, and why the order of the layers matters.

A coffee shop sells espresso with optional milk, sugar and caramel. Model each combination as a subclass and you get EspressoWithMilk, EspressoWithMilkAndSugar, EspressoWithSugarAndCaramel... Three add-ons already mean eight classes, and every new add-on doubles the count.

The other obvious fix - boolean flags on one big class - means every new add-on edits that class, and the pricing method turns into a pile of if statements.

We'll go from simple to more technical:

  1. The pattern: component, concrete component and decorator - and how it differs from Python's @ syntax.
  2. How a stack of decorators handles a call, step by step.
  3. Where you'll see decorators in the standard libraries and frameworks.
  4. Order, identity and the other ways stacks surprise people.

The pattern

A Decorator wraps an object that has the same interface and adds one piece of behavior before or after passing the call along. Because the wrapper is itself a valid object of that interface, wrappers can be stacked.

Not Python's @ syntax. A Python decorator (@functools.cache, @app.get("/")) is a function that wraps another function once, when it is defined. The idea is the same - wrap, forward, add one thing - but this pattern wraps objects, at run time, and each object can get a different stack.

  • Component - the shared interface (Coffee).
  • Concrete component - the plain object (Espresso).
  • Decorator - implements Coffee and holds another Coffee.

Java

interface Coffee {
    double cost();
    String description();
}

class Espresso implements Coffee {
    public double cost() { return 2.00; }
    public String description() { return "Espresso"; }
}

abstract class CoffeeDecorator implements Coffee {
    protected final Coffee inner;
    CoffeeDecorator(Coffee inner) { this.inner = inner; }
}

class Milk extends CoffeeDecorator {
    Milk(Coffee inner) { super(inner); }
    public double cost() { return inner.cost() + 0.50; }
    public String description() { return inner.description() + ", milk"; }
}

Coffee order = new Sugar(new Milk(new Espresso()));   // any combination, built at runtime

Python

from abc import ABC, abstractmethod


class Coffee(ABC):
    @abstractmethod
    def cost(self) -> float: ...

    @abstractmethod
    def description(self) -> str: ...


class Espresso(Coffee):
    def cost(self) -> float:
        return 2.00

    def description(self) -> str:
        return "Espresso"


class CoffeeDecorator(Coffee):
    def __init__(self, inner: Coffee):
        self._inner = inner


class Milk(CoffeeDecorator):
    def cost(self) -> float:
        return self._inner.cost() + 0.50

    def description(self) -> str:
        return self._inner.description() + ", milk"


order = Sugar(Milk(Espresso()))   # any combination, built at runtime

How it works, step by step

  1. All objects share one interface, so a decorated coffee is still a Coffee.
  2. Each decorator forwards the call to the object it wraps...
  3. ...and adds its own work before or after (here: add its price, append its name).
  4. Wrap as many times as you like; the outermost decorator is called first.
  5. Adding a new add-on means writing one new decorator class. Nothing else changes.

Where you'll see it

  • java.io: new BufferedReader(new InputStreamReader(new FileInputStream(file))) - each layer adds buffering or decoding.
  • Collections.unmodifiableList(list) and Collections.synchronizedList(list) return decorators around your list.
  • In Python, io.BufferedReader(raw) and gzip.GzipFile(fileobj=f) wrap file objects the same way, and WSGI/ASGI middleware wraps a web app.
  • Web middleware and servlet filters follow the same idea: each layer adds logging, compression or authentication around the next.

Choosing among the structural patterns

If you need to...Reach forWhat it changes
Make an existing class fit the interface your code expectsAdapterChanges the interface; the behavior stays the same.
Add behavior to an object without changing its class or interfaceDecorator (this lesson)Same interface, extra behavior, and wrappers can be stacked.
Give a complicated subsystem one simple front doorFacadeA new, simpler interface over many classes.
Treat a single item and a group of items the same wayCompositeA tree: groups contain items or other groups.
Control access to an object (lazy loading, caching, security, remote calls)ProxySame interface, but it decides when and whether the real object is used.

Common mistakes

MistakeWhat happensDo this instead
Stacking layers in an arbitrary orderencrypt-then-compress barely compresses; a cache outside an auth check serves other users' datadecide the order deliberately and test it
==, equals or instanceof / is, isinstance on a decorated objectthe checks see the wrapper, not the originaldon't rely on identity or concrete type
A subclass per combination of options2^N classesone decorator per option
Decorators that forget to forward a methodthe wrapped object's behavior is silently skippedforward everything (a forwarding base class helps)
Deep stacks nobody can followunreadable stack traceskeep stacks short and assembled in one place

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Four optional add-ons. How many subclasses cover every combination, and how many decorators?

Show answer

2^4 = 16 subclasses, counting the plain drink - or 4 decorators, combined at runtime.

2. new Encrypting(new Compressing(file)) - which transformation does written data go through first?

Show answer

Encryption: a write reaches the outermost decorator first. So this encrypts and then compresses - and encrypted bytes barely compress. Swap the order.

3. Why can coffee instanceof Espresso be false for a decorated espresso?

Show answer

The object you hold is the outermost decorator (say, Milk), which wraps the espresso. It is a different object of a different class, so identity and type checks against the original fail.

Key takeaways

  • A decorator wraps an object with the same interface and adds one behavior.
  • Decorators stack, so N options need N classes instead of 2^N subclasses.
  • The order of wrapping changes the result - choose it deliberately.
  • A decorated object is a different object: don't rely on == or instanceof for it.
  • java.io streams are the textbook example.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

You need to...Reach for
Add optional behaviors in any combinationDecorator - one class per behavior
Add the same behavior to every method (timing, logging)a dynamic proxy, aspect or middleware
Wrap a function once, at definitiona Python @ decorator (with functools.wraps)
Control access (lazy, cache, auth, remote)Proxy
Let handlers decide whether to pass a request onChain of Responsibility

Snippets worth knowing by heart

Java

abstract class CoffeeDecorator implements Coffee {
    protected final Coffee inner;
    CoffeeDecorator(Coffee inner) { this.inner = inner; }
}
final class Milk extends CoffeeDecorator {
    Milk(Coffee inner) { super(inner); }
    public double cost() { return inner.cost() + 0.50; }
    public String description() { return inner.description() + ", milk"; }
}
Coffee order = new Sugar(new Milk(new Espresso()));

Python

def timed(f):
    @functools.wraps(f)              # keep __name__, __doc__, __wrapped__
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return f(*args, **kwargs)
        finally:
            log.info("%s took %.3fs", f.__name__, time.perf_counter() - start)
    return wrapper

The traps

  • Arbitrary layer order - compression, caching and auth all depend on it.
  • Identity and type checks see the wrapper.
  • Forgotten forwarding - especially for methods added later.
  • Deep stacks - assemble them in one place.

Staff-level points, one line each

  • Text: 110,000 bytes compressed to 312; random bytes to 110,041 - compress before encrypting.
  • Metrics outside a retry counted 1 call; inside, 3 attempts - know which you're measuring.
  • Cache(Auth(store)) served Ana's data to Ben; Auth(Cache(store)) refused him.
  • Self-calls inside the wrapped object bypass the decorator (and framework proxies).
  • New default methods aren't forwarded by old decorators.
  • java.sql.Wrapper.unwrap and inspect.unwrap reach the wrapped object deliberately.
  • synchronizedList makes each call atomic, not check-then-act or iteration.
  • @a @b def f is a(b(f)); skipping functools.wraps renamed the function to w.

Say it in an interview

  • "Each add-on is one decorator, so four options are four classes, not sixteen."
  • "Authorization wraps the cache, never the other way round."
  • "This counter sits outside the retry, so it counts requests, not attempts."
  • "The stack is assembled in one place and tested as a whole."

Practice

This skill comes with 9 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Decorator Pattern free

Price a Coffee Order via the Decorator Pattern

MediumProCoding problemJava · Python

A SimpleCoffee costs 2.00. Each add-on wraps the coffee in a decorator that adds to its cost: MILK adds 0.50, SUGAR adds 0.25, WHIP adds 0.75.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Build a Text Pipeline From Stackable Decorators

MediumProCoding problemJava · Python

Each class below is a decorator: it is a TextSource and it wraps another TextSource. read() should ask the wrapped source for its text, transform it, and return the result.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Decorator Pattern free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime