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.

Overview ยท Structural ยท Singleton ยท Factory Method ยท Abstract Factory ยท Builder ยท Prototype ยท Adapter ยท Bridge ยท Composite ยท Decorator ยท Facade ยท Flyweight ยท Proxy ยท Observer ยท Strategy ยท Command ยท Template Method ยท Chain of Resp. ยท State ยท Mediator ยท Iterator ยท Visitor ยท Memento ยท Interpreter
Category: Structural Difficulty: Intermediate Interview: Tier 1 Confused with: Proxy, Composite
01
Section One ยท The Problem

Why Decorator Exists

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 features class EmailNotifier { ... } class EmailSmsNotifier extends EmailNotifier { ... } class EmailSlackNotifier extends EmailNotifier { ... } class EmailSmsSlackNotifier extends EmailNotifier { ... } class EmailSmsSlackLoggingNotifier extends EmailNotifier { ... } // โœ— 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
Notifier EmailSmsNotifier EmailSlackNotifier EmailSmsSlackNotifier EmailSmsSlackLogging... ✗ 2ⁿ subclasses for N features ✗ Can't add behaviour at runtime ✗ Single inheritance blocks mixing ✗ Base class change โ†’ all subclasses break

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
«interface» Beverage + cost() : double + description() : String Espresso - price : double + cost() โ†’ 2.00 «abstract» BeverageDecorator - wrapped : Beverage + cost() โ†’ wrapped.cost() MilkDecorator + cost() โ†’ +0.50 WhipDecorator + cost() โ†’ +0.70 wrapped Client uses ■ Blue = Component interface ■ Green = ConcreteComponent ■ Gold = Decorator hierarchy --▷ dashed = implements
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.
Decorator — Wrapping Chain (Java I/O)
DataInputStream (adds readInt, readDouble, readUTF) BufferedInputStream (adds 8KB buffer) FileInputStream("data.bin") client.readInt() โ†’ DataIn delegates โ†’ BufferedIn delegates โ†’ FileIn reads disk
The pattern in code — Java I/O decorator stacking
// โ”€โ”€ Layer 1: raw bytes from file (ConcreteComponent) โ”€โ”€ InputStream fileIn = new FileInputStream("data.bin"); // โ”€โ”€ Layer 2: add buffering (Decorator #1) โ”€โ”€ InputStream buffIn = new BufferedInputStream(fileIn); // โ”€โ”€ Layer 3: add typed reads (Decorator #2) โ”€โ”€ DataInputStream dataIn = new DataInputStream(buffIn); // โ”€โ”€ Client uses the outermost layer โ”€โ”€ int value = dataIn.readInt(); // reads 4 buffered bytes โ†’ assembles int String s = dataIn.readUTF(); // reads length-prefixed UTF string dataIn.close(); // closes entire chain // โ”€โ”€ Swap base: same decorators, different source โ”€โ”€ byte[] testData = { 0, 0, 0, 42 }; DataInputStream testIn = new DataInputStream( new BufferedInputStream( new ByteArrayInputStream(testData))); System.out.println(testIn.readInt()); // โ†’ 42
Java’s entire I/O library is Decorator:
  • InputStream is the 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 1 java.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 2 Collections.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 3 javax.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 = new ArrayList<>(List.of("a", "b", "c")); // Decorator #1: make it unmodifiable List<String> safe = Collections.unmodifiableList(base); // Decorator #2: make it synchronized (thread-safe) List<String> synced = Collections.synchronizedList(base); // Decorator #3: add type checking at runtime List<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 deep try (Writer w = new BufferedWriter( new OutputStreamWriter( new GZIPOutputStream( new FileOutputStream("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.

Java — Decorator Pattern Coffee Shop (core)
// โ”€โ”€ Component interface โ”€โ”€ interface Beverage { double cost(); String description(); } // โ”€โ”€ ConcreteComponent โ”€โ”€ record Espresso() implements Beverage { public double cost() { return 2.00; } public String description() { return "Espresso"; } } // โ”€โ”€ Abstract Decorator โ”€โ”€ abstract class BeverageDecorator implements Beverage { protected final Beverage wrapped; BeverageDecorator(Beverage wrapped) { this.wrapped = wrapped; } public double cost() { return wrapped.cost(); } public String description() { return wrapped.description(); } } // โ”€โ”€ ConcreteDecorators โ”€โ”€ class MilkDecorator extends BeverageDecorator { MilkDecorator(Beverage b) { super(b); } public double cost() { return wrapped.cost() + 0.50; } public String description() { return wrapped.description() + ", Milk"; } } class WhipDecorator extends BeverageDecorator { WhipDecorator(Beverage b) { super(b); } public double cost() { return wrapped.cost() + 0.70; } public String description() { return wrapped.description() + ", Whip"; } }
Why an abstract Decorator class?:
  • 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 description class MilkDecorator extends BeverageDecorator { public double cost() { 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 = new MilkDecorator(new Espresso()); if (drink instanceof Espresso) { // โœ— 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
PatternStructureIntentChildren
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.

  • Medium decorator Logging HTTP Client — Wrap an HttpClient interface with a decorator that logs request/response details. Add a timing decorator that measures latency. Stack both.
  • Easy decorator Text Formatter — Create a TextSource interface with getText(). Implement decorators: UpperCaseDecorator, TrimDecorator, BracketDecorator (wraps text in [ ]). Verify order-independence.
  • Medium decorator Repository 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.
  • Hard decorator Custom 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.
  • Medium decorator Pizza 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.”