On this page
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:
- The pattern: component, concrete component and decorator - and how it differs from Python's
@syntax. - How a stack of decorators handles a call, step by step.
- Where you'll see decorators in the standard libraries and frameworks.
- 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
Coffeeand holds anotherCoffee.
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
- All objects share one interface, so a decorated coffee is still a
Coffee. - Each decorator forwards the call to the object it wraps...
- ...and adds its own work before or after (here: add its price, append its name).
- Wrap as many times as you like; the outermost decorator is called first.
- 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)andCollections.synchronizedList(list)return decorators around your list.- In Python,
io.BufferedReader(raw)andgzip.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 for | What it changes |
|---|---|---|
| Make an existing class fit the interface your code expects | Adapter | Changes the interface; the behavior stays the same. |
| Add behavior to an object without changing its class or interface | Decorator (this lesson) | Same interface, extra behavior, and wrappers can be stacked. |
| Give a complicated subsystem one simple front door | Facade | A new, simpler interface over many classes. |
| Treat a single item and a group of items the same way | Composite | A tree: groups contain items or other groups. |
| Control access to an object (lazy loading, caching, security, remote calls) | Proxy | Same interface, but it decides when and whether the real object is used. |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| Stacking layers in an arbitrary order | encrypt-then-compress barely compresses; a cache outside an auth check serves other users' data | decide the order deliberately and test it |
==, equals or instanceof / is, isinstance on a decorated object | the checks see the wrapper, not the original | don't rely on identity or concrete type |
| A subclass per combination of options | 2^N classes | one decorator per option |
| Decorators that forget to forward a method | the wrapped object's behavior is silently skipped | forward everything (a forwarding base class helps) |
| Deep stacks nobody can follow | unreadable stack traces | keep 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
==orinstanceoffor it. java.iostreams are the textbook example.