Decorator โ Structural design pattern with UML diagrams, Java implementation, and real-world examples.
Structural
Decorator Pattern
Attach additional responsibilities to an object dynamically. An intermediate structural pattern — the foundation of Java I/O streams, servlet filters, and any system where you stack behaviours without modifying existing classes.
You are building a notification system. Users can receive notifications via email. Some users also want SMS. Some want Slack. Some want all three — plus logging. The inheritance approach: create a subclass for every combination. EmailNotifier, EmailSmsNotifier, EmailSlackNotifier, EmailSmsSlackNotifier, EmailSmsSlackLoggingNotifier... With N optional behaviours, you face 2N subclasses. Each new feature doubles the number of classes.
Naive approach — subclass explosion
// โ One subclass per combination of featuresclassEmailNotifier { ... }
classEmailSmsNotifierextendsEmailNotifier { ... }
classEmailSlackNotifierextendsEmailNotifier { ... }
classEmailSmsSlackNotifierextendsEmailNotifier { ... }
classEmailSmsSlackLoggingNotifierextendsEmailNotifier { ... }
// โ 5 features โ 32 subclasses. 6 features โ 64.// โ Java doesn't support multiple inheritance โ can't mix freely.
What goes wrong:
Combinatorial explosion — N optional features need 2N subclasses to cover every combination
Static binding — the combination is frozen at compile time; you cannot add SMS to an already-constructed email notifier at runtime
Single inheritance wall — Java allows only one superclass, so mixing features via inheritance is impossible beyond one axis
Fragile hierarchy — changing the base class ripples through dozens of subclasses
Without Decorator — combinatorial subclass explosion
This is the problem Decorator solves — wrap the base object in layers, each adding one behaviour. Need email + SMS + logging? Wrap: new LoggingDecorator(new SmsDecorator(new EmailNotifier())). Three objects, not 23 subclasses. Add or remove layers at runtime. Each decorator adds one concern; combine freely.
02
Section Two ยท The Pattern
What Is Decorator?
Decorator is a structural pattern that lets you attach additional responsibilities to an object dynamically by wrapping it in another object that shares the same interface. The wrapper delegates to the wrapped object and adds its own behaviour before or after the delegation. You can stack multiple wrappers — each adds one concern without the others knowing.
GoF Intent: “Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.” — Gamma, Helm, Johnson, Vlissides (1994)
Analogy — coffee shop: You order an espresso. The barista starts with the base drink (ConcreteComponent). You ask for milk — they don’t make a new “MilkEspresso” class. They pour milk into the existing cup (first decorator). You add whipped cream — another layer on top (second decorator). Each addition wraps the previous result. The final drink is still “a beverage” — it has a cost and a description — but each layer added something. Remove whip? Remove one wrapper. Add caramel? Add one wrapper. No subclass for every combination of toppings.
Key insight: Decorator works because the wrapper and the wrapped object share the same interface. The client holds a Beverage reference — it doesn’t know if it’s a bare Espresso or an Espresso wrapped in three decorators. Each decorator stores a reference to a Component, delegates all interface calls to it, and adds its own behaviour. This is composition over inheritance in its purest form.
Decorator implements the same interface as the component it wraps
▶
The client treats decorated and undecorated objects identically — substitutable via polymorphism
Decorator holds a reference to a Component (the wrapped object)
▶
It delegates interface calls to the wrapped object, then adds its own behaviour before/after
Each decorator adds one responsibility
▶
Combine behaviours by stacking decorators — N features need N classes, not 2N
Decorators can be added or removed at runtime
▶
Behaviour composition is dynamic, not frozen at compile time like inheritance
Decorator vs. Composite:
Both use recursive composition with a shared interface.
The difference: Decorator wraps one object to add behaviour (a chain — each layer delegates to the next).
Composite holds many children to form a tree (each node delegates an operation to all its children).
Decorator = one child, behaviour stacking. Composite = many children, tree traversal.
03
Section Three ยท Anatomy
Participants & Structure
Participant
Role
In the Analogy
Component
The common interface declaring operations shared by concrete components and decorators (e.g. cost(), description()). The client programs to this interface exclusively.
The “beverage” abstraction — anything you can drink has a cost and a description.
ConcreteComponent
The base object being decorated. Implements the Component interface with its core behaviour (e.g. Espresso returns its own cost).
The plain espresso — the drink before any add-ons.
Decorator
Abstract class (or interface) that implements Component and holds a reference to a Component. Delegates all calls to the wrapped object. Subclasses add specific behaviour.
The barista station where add-ons are applied — it passes the cup through unchanged unless a specific decorator adds something.
ConcreteDecorator
Extends Decorator, adding one specific responsibility. Calls super.operation() (which delegates to the wrapped component) then adds its own behaviour (e.g. MilkDecorator adds $0.50 to cost).
A specific add-on: milk, whipped cream, caramel — each one wraps the cup and adds its contribution.
Client
Creates the component, wraps it in decorators, and works with the Component interface. Never knows how many layers exist.
The customer — orders a beverage, doesn’t care how many wrappers the barista applied internally.
Decorator — UML Class Diagram
The delegation chain is the key:
WhipDecorator holds a Beverage reference (which might be a MilkDecorator, which holds another Beverage reference pointing to Espresso).
Calling cost() on Whip calls wrapped.cost() (Milk), which calls wrapped.cost() (Espresso).
Each layer adds its price on top. The client sees one Beverage object with the combined cost.
04
Section Four ยท How It Works
The Pattern In Motion
Scenario: Java I/O streams. You start with raw bytes from a file. You want buffering (performance), then data type parsing (convenience). Each layer wraps the previous — classic Decorator.
Step 1 — Create the base component: new FileInputStream("data.bin")
▶
ConcreteComponent — reads raw bytes from disk. No buffering, no parsing. The core object.
Step 2 — Wrap with first decorator: new BufferedInputStream(fileIn)
▶
First decorator — adds an 8KB internal buffer. Reads ahead in bulk, serves subsequent read() calls from memory. Delegates to fileIn when buffer empties.
Step 3 — Wrap with second decorator: new DataInputStream(buffIn)
▶
Second decorator — adds readInt(), readDouble(), readUTF(). Reads multiple bytes from the layer below and assembles them into typed values.
Step 4 — Client calls dataIn.readInt()
▶
DataInputStream asks BufferedInputStream for 4 bytes. BufferedInputStream serves from its buffer (or reads a block from FileInputStream). DataInputStream assembles the 4 bytes into an int. The client gets a typed value — two layers of decoration invisible.
Step 5 — Swap the base: replace FileInputStream with ByteArrayInputStream
▶
The decorators work identically on any InputStream. Buffering and parsing don’t care about the source — the same wrappers compose with any base component.
FileInputStream, ByteArrayInputStream are ConcreteComponents.
FilterInputStream is the abstract Decorator.
BufferedInputStream, DataInputStream, GZIPInputStream are ConcreteDecorators.
Every time you nest constructors in Java I/O, you’re stacking decorators.
05
Section Five ยท Java Stdlib
You Already Use This
Decorator is one of the most-used patterns in the JDK. Any time you wrap an object to add behaviour without subclassing, that’s Decorator at work.
IN JAVA
Example 1java.io — the textbook Decorator. InputStream (Component), FileInputStream (ConcreteComponent), FilterInputStream (abstract Decorator), BufferedInputStream / DataInputStream / GZIPInputStream (ConcreteDecorators). Same pattern for OutputStream, Reader, Writer. Stacking: new PrintWriter(new BufferedWriter(new FileWriter("out.txt"))).
Example 2Collections.unmodifiableList(list) — wraps a List and throws UnsupportedOperationException on mutation methods. The wrapper implements List (same interface), delegates reads to the wrapped list, blocks writes. Same pattern: synchronizedList(), checkedList().
Example 3javax.servlet.http.HttpServletRequestWrapper — the abstract Decorator for HTTP requests. Servlet filters use it to modify request attributes, headers, or parameters without touching the original request object. Each filter wraps the request, adds/overrides behaviour, passes it down the chain.
Stdlib usage — Collections decorators
// Base list (ConcreteComponent)List<String> base = newArrayList<>(List.of("a", "b", "c"));
// Decorator #1: make it unmodifiableList<String> safe = Collections.unmodifiableList(base);
// Decorator #2: make it synchronized (thread-safe)List<String> synced = Collections.synchronizedList(base);
// Decorator #3: add type checking at runtimeList<String> checked = Collections.checkedList(base, String.class);
// All return List<String> โ same interface, different guarantees
safe.add("x"); // โ UnsupportedOperationException
Stdlib usage — I/O stream stacking
// Write compressed, buffered text to a file โ 3 decorators deeptry (Writer w = newBufferedWriter(
newOutputStreamWriter(
newGZIPOutputStream(
newFileOutputStream("log.gz"))))) {
w.write("compressed and buffered");
}
// FileOutputStream โ GZIPOutputStream โ OutputStreamWriter โ BufferedWriter// Each layer adds one concern: disk, compression, encoding, buffering.
Why Java I/O feels verbose:
The constructor-nesting pattern (new A(new B(new C()))) is the cost of Decorator’s flexibility.
Each layer is independent and recombinable.
Compare to a monolithic BufferedCompressedFileWriter class that fuses concerns and can’t be decomposed.
The verbosity is the price of composability.
06
Section Six ยท Implementation
Build It Once
Domain: Coffee pricing. A Beverage interface, a base Espresso, and decorators for Milk, Whip, and Caramel — each adding cost and description.
BeverageDecorator provides the default delegation (cost() โ wrapped.cost()).
ConcreteDecorators override only the methods they enhance.
Without the abstract class, every decorator must manually delegate every interface method — tedious and error-prone when the interface has many methods.
07
Section Seven ยท Watch Out
Common Mistakes
Mistake #1 — Not forwarding all interface methods: If the Component interface has 10 methods and your decorator only overrides 2, those other 8 must still delegate to the wrapped object. Forgetting to forward them means those methods hit the decorator’s default (often returning null or zero). Fix: Use an abstract decorator base that delegates everything; concrete decorators override only what they add.
✗ Wrong — decorator forgets to delegate toString()
// โ MilkDecorator doesn't override toString()// โ returns "MilkDecorator@1a2b3c" instead of the beverage descriptionclassMilkDecoratorextendsBeverageDecorator {
public doublecost() { return wrapped.cost() + 0.50; }
// forgot description() โ base class delegates, but toString() doesn't
}
Mistake #2 — Using instanceof on decorated objects:
beverage instanceof Espresso returns false on a decorated espresso because the outer object is MilkDecorator, not Espresso.
Decorator intentionally hides the concrete type.
Fix: Program to the Component interface. If you need identity, add an isType() method or use an unwrap utility — but needing instanceof on a decorated object usually signals a design problem.
✗ Wrong — instanceof fails on decorated objects
Beverage drink = newMilkDecorator(newEspresso());
if (drink instanceofEspresso) { // โ FALSE โ outer type is MilkDecorator
System.out.println("It's an espresso!");
}
// โ Decorator wraps the type โ identity is lost// โ Don't check types; work with the Beverage interface only
Mistake #3 — Order-dependent decorators without documentation:
If SizeDecorator multiplies cost and MilkDecorator adds a flat fee, the stacking order matters: new Milk(new Size(espresso)) costs differently than new Size(new Milk(espresso)).
Decorators are usually intended to be order-independent, but when they’re not, the API must make the correct order obvious (builder pattern, factory method, or clear documentation).
Mistake #4 — Deep decorator stacks make debugging painful:
A stack of 5+ decorators means a stack trace with 5+ delegation calls.
Logging and breakpoints become confusing.
Fix: If you regularly stack more than 3 decorators, consider whether some behaviours should be merged into a single decorator or whether a different pattern (strategy with a pipeline) fits better.
08
Section Eight ยท Decision Guide
When To Use Decorator
You need to add responsibilities dynamically at runtime
▶
Use Decorator — wrap or unwrap layers without changing compiled class hierarchy
Extending via subclassing would cause combinatorial explosion
▶
Use Decorator — N features need N decorator classes, not 2N subclasses
You want to add cross-cutting behaviour (logging, caching, auth) without modifying existing code
▶
Use Decorator — wrap the service in a logging/caching decorator; Open/Closed principle preserved
You need to control access (not add behaviour)
▶
Use Proxy instead — same wrapping structure, but intent is access control, not feature addition
You need to treat trees of objects uniformly
▶
Use Composite instead — one-to-many children, not one-to-one wrapping
Pattern
Structure
Intent
Children
Decorator
Wraps one object
Add behaviour dynamically
Exactly 1 (the wrapped object)
Proxy
Wraps one object
Control access (lazy load, security, remote)
Exactly 1
Composite
Tree of objects
Treat part-whole hierarchies uniformly
0..N children
Adapter
Wraps one object
Convert interface to expected interface
Exactly 1
09
Section Nine ยท Practice
Problems To Solve
Decorator appears whenever you need to layer behaviours onto an object without modifying it. These exercises build intuition for identifying wrapping opportunities.
MediumdecoratorLogging HTTP Client — Wrap an HttpClient interface with a decorator that logs request/response details. Add a timing decorator that measures latency. Stack both.
EasydecoratorText Formatter — Create a TextSource interface with getText(). Implement decorators: UpperCaseDecorator, TrimDecorator, BracketDecorator (wraps text in [ ]). Verify order-independence.
MediumdecoratorRepository Caching — Wrap a UserRepository with a CachingDecorator that serves from an in-memory map. Add a LoggingDecorator. Discuss which should be the outer layer and why.
HarddecoratorCustom InputStream — Implement a CountingInputStream decorator that tracks total bytes read. Implement a ProgressInputStream that fires a callback every N bytes. Stack both over a FileInputStream.
MediumdecoratorPizza Pricing — Model a pizza ordering system where base pizzas (Margherita, Pepperoni) are decorated with toppings (ExtraCheese, Mushrooms, Olives). Calculate total price and generate an itemized receipt.
The key insight for Decorator problems:
If you catch yourself writing if (hasFeatureX) checks inside a class, or creating subclasses for every feature combination, extract each feature into a decorator.
One class per behaviour, stackable at runtime.
The pattern unlocks when you see “optional, combinable enhancements to an object’s capabilities.”