The menu of La Bella Napoli is now modeled as a tree, but the customer doesn't order "the Margarita from the menu": they order their Margarita — with double cheese, gluten-free, large portion. Each extra modifies the price and the description, the extras combine freely, and tomorrow there will be new extras. Modeling this with subclasses means condemning yourself to a combinatorial explosion; modeling it with boolean flags means condemning the class to grow without end. Decorator offers the third way: wrap the object in layers that add responsibilities, composable at runtime, without touching or inheriting from the original class. And as a bonus we'll see that the same trick solves a technical problem left over from module 2: adding retries and logging to the notifiers.
Contents
- The problem in PideYa: dish extras
- Why inheritance can't handle this
- Intent and structure of the pattern
- Complete Java implementation
- Technical decorators: retries and logging over
Notifier - Decorator in the JDK: the java.io streams
- When to use it and when not to
- Common mistakes, exercises, and conclusion
The problem in PideYa: dish extras
When a customer adds a Margarita to their order, they can customize it:
| Extra | Effect on the price | Effect on the description |
|---|---|---|
| Double cheese | +€1.50 | "... with double cheese" |
| Gluten-free adaptation | +€2.00 | "... (gluten-free)" |
| Large portion | +30% on the running total | "... large portion" |
The extras combine in any subset (and the "+30%" means even the order of application matters). What travels in each OrderLine of the Order we built in module 2 must answer two questions: what description do I print on the receipt? what price do I charge? In other words, in the order domain a customized dish is a Product:
The Dish from the previous lesson's menu implements Product trivially (its name, its base price). The problem is the combinations.
Why inheritance can't handle this
Attempt 1: subclasses. DoubleCheeseMargarita, GlutenFreeMargarita, GlutenFreeDoubleCheeseMargarita, LargeGlutenFreeDoubleCheeseMargarita... With 3 optional extras there are 2³ = 8 combinations per dish; with 5 extras, 32. And everything duplicates again for the Prosciutto, the Carbonara... Inheritance fixes the combinations at compile time, while the extras are decided at runtime, customer by customer. It is the multiplicative explosion we already smelled in Bridge, aggravated: there it was two axes; here, one axis per extra.
Attempt 2: flags on the class. Dish with boolean doubleCheese, glutenFree, largePortion and a getPrice() full of ifs. It works... until marketing invents the "bacon" extra and Dish must be modified (goodbye OCP), and the class accumulates the responsibilities of every present and future extra (goodbye SRP). On top of that, every dish hauls around fields that 90% of orders never use.
The need, stated precisely: add responsibilities (extra price, extra text) to individual objects, not to classes, in any combination, decided at runtime, and without modifying existing code.
Intent and structure of the pattern
Intent (GoF): attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
The mechanism has a beautiful symmetry: the decorator implements the same interface it decorates and contains an object of that interface. It is simultaneously a Product (outward) and a user of Product (inward). That is why decorators stack: each layer wraps the previous one without knowing whether it wraps the base object or another layer.
classDiagram
class Product {
<<interface>>
+getDescription() String
+getPrice() BigDecimal
}
class Dish {
+getDescription() String
+getPrice() BigDecimal
}
class DishExtra {
<<abstract>>
#wrapped: Product
+DishExtra(wrapped: Product)
+getDescription() String
+getPrice() BigDecimal
}
class DoubleCheeseExtra {
+getDescription() String
+getPrice() BigDecimal
}
class GlutenFreeAdaptation {
+getDescription() String
+getPrice() BigDecimal
}
class LargePortionExtra {
+getDescription() String
+getPrice() BigDecimal
}
Product <|.. Dish
Product <|.. DishExtra
DishExtra <|-- DoubleCheeseExtra
DishExtra <|-- GlutenFreeAdaptation
DishExtra <|-- LargePortionExtra
DishExtra o-- Product : wraps
| GoF role | In PideYa |
|---|---|
| Component (common interface) | Product |
| ConcreteComponent (the base object) | Dish |
| Decorator (abstract base holding the reference) | DishExtra |
| ConcreteDecorator | DoubleCheeseExtra, GlutenFreeAdaptation, LargePortionExtra |
Notice the two arrows on DishExtra: it implements Product and it contains a Product. That double relationship with the same interface is the pattern's visual signature (compare it with Composite: there the group contained many Components; the decorator contains exactly one — the GoF calls Decorator "a degenerate composite with only one child").
Complete Java implementation
The concrete component we already have: Dish from the menu, implementing Product by returning its name and base price.
The base decorator concentrates the plumbing (store the reference, delegate by default):
public abstract class DishExtra implements Product {
protected final Product wrapped;
protected DishExtra(Product wrapped) {
this.wrapped = Objects.requireNonNull(wrapped);
}
// By default, delegate: each concrete decorator overrides what it alters.
@Override public String getDescription() { return wrapped.getDescription(); }
@Override public BigDecimal getPrice() { return wrapped.getPrice(); }
}The concrete decorators — each one is tiny, does exactly one thing (SRP), and always calls the wrapped object:
public class DoubleCheeseExtra extends DishExtra {
private static final BigDecimal SURCHARGE = new BigDecimal("1.50");
public DoubleCheeseExtra(Product wrapped) { super(wrapped); }
@Override
public String getDescription() { return wrapped.getDescription() + " with double cheese"; }
@Override
public BigDecimal getPrice() { return wrapped.getPrice().add(SURCHARGE); }
}
public class GlutenFreeAdaptation extends DishExtra {
private static final BigDecimal SURCHARGE = new BigDecimal("2.00");
public GlutenFreeAdaptation(Product wrapped) { super(wrapped); }
@Override
public String getDescription() { return wrapped.getDescription() + " (gluten-free)"; }
@Override
public BigDecimal getPrice() { return wrapped.getPrice().add(SURCHARGE); }
}
public class LargePortionExtra extends DishExtra {
private static final BigDecimal FACTOR = new BigDecimal("1.30");
public LargePortionExtra(Product wrapped) { super(wrapped); }
@Override
public String getDescription() { return wrapped.getDescription() + ", large portion"; }
@Override
public BigDecimal getPrice() {
return wrapped.getPrice().multiply(FACTOR).setScale(2, RoundingMode.HALF_UP);
}
}The client composes at runtime, layer by layer, following whatever the user ticks in the app:
Product order = new Dish("Margarita", new BigDecimal("8.50"));
order = new DoubleCheeseExtra(order);
order = new GlutenFreeAdaptation(order);
order = new LargePortionExtra(order);
System.out.println(order.getDescription());
// Margarita with double cheese (gluten-free), large portion
System.out.println(order.getPrice());
// (8.50 + 1.50 + 2.00) * 1.30 = 15.60Each call to getPrice() travels through the onion from the outside in, and the result is built from the inside out:
sequenceDiagram
participant App
participant Large as :LargePortionExtra
participant GlutenFree as :GlutenFreeAdaptation
participant Cheese as :DoubleCheeseExtra
participant Marg as :Dish
App->>Large: getPrice()
Large->>GlutenFree: getPrice()
GlutenFree->>Cheese: getPrice()
Cheese->>Marg: getPrice()
Marg-->>Cheese: 8.50
Cheese-->>GlutenFree: 10.00
GlutenFree-->>Large: 12.00
Large-->>App: 15.60
The pattern's final tally: 3 classes cover the 8 combinations (and the 16 when bacon arrives: one more class). The new extra touches nothing existing (OCP), each extra lives in its own class (SRP), and since everything is a Product, module 2's OrderLine stores it without ever learning how many layers it carries. Note too that the order matters and the model expresses it: large portion applied last multiplies the extras; applied first, only the base price. That, which was impossible to express with boolean flags, is here simply the wrapping order.
Technical decorators: retries and logging over Notifier
The same pattern shines far from the menu. Pending since module 2: notification deliveries sometimes fail (networks, providers down) and we want retries and traces in EventLog... without touching PushNotifier, SmsNotifier, or EmailNotifier, and without duplicating that logic in all three. Decorators over the Notifier interface:
public class RetryNotifier implements Notifier {
private final Notifier wrapped;
private final int maxAttempts;
public RetryNotifier(Notifier wrapped, int maxAttempts) {
this.wrapped = wrapped;
this.maxAttempts = maxAttempts;
}
@Override
public void send(String recipient, String message) {
NotificationException last = null;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
wrapped.send(recipient, message);
return; // success: we're done
} catch (NotificationException e) {
last = e; // failure: next attempt
}
}
throw new NotificationException("Exhausted " + maxAttempts + " attempts", last);
}
}
public class LoggingNotifier implements Notifier {
private final Notifier wrapped;
public LoggingNotifier(Notifier wrapped) { this.wrapped = wrapped; }
@Override
public void send(String recipient, String message) {
EventLog.INSTANCE.info("Sending to " + recipient);
try {
wrapped.send(recipient, message);
EventLog.INSTANCE.info("Sent OK to " + recipient);
} catch (NotificationException e) {
EventLog.INSTANCE.error("Failed sending to " + recipient + ": " + e.getMessage());
throw e;
}
}
}A typical composition (and notice that the order means something again: logging outside the retries traces the final outcome; inside, each attempt):
And the connection with the creational patterns is direct: the Supplier-based NotifierRegistry can register the channel already decorated (() -> new LoggingNotifier(new RetryNotifier(new SmsNotifier(), 3))), so that no client ever knows how many layers its notifier carries. This family of technical decorators (retries, logging, metrics, caching, encryption) is among the pattern's most profitable uses in real systems — with a close relative, Proxy, that we will tell apart in its lesson: same mechanics, different intent.
Decorator in the JDK: the java.io streams
The pattern's canonical example has been in Java since 1996. All of I/O is built as decorators over InputStream/OutputStream:
InputStream input =
new BufferedInputStream( // decorator: adds buffering
new GZIPInputStream( // decorator: adds decompression
new FileInputStream("orders-2026.csv.gz"))); // concrete componentBufferedInputStream and GZIPInputStream implement InputStream and wrap an InputStream (via the FilterInputStream base, which is literally the Decorator role): the exact double relationship of our DishExtra. The combinations (buffered compressed file, unbuffered encrypted socket...) would be unmanageable through inheritance; through decoration they are one line. In the same family: Collections.unmodifiableList(...) and Collections.synchronizedList(...) decorate a List adding immutability or synchronization. Whenever you chain constructors that take "the same thing they return", you are decorating.
When to use it and when not to
Use it when:
- There are optional, combinable responsibilities on an object (dish extras; buffering/compression/encryption on streams) and the combinations are decided at runtime.
- You want to add cross-cutting behavior (retries, traces, metrics) to existing implementations without modifying them and without duplicating it in each one.
- Inheritance is impracticable: combinatorial explosion, or the class is
final, or you want to decorate specific instances rather than all of them.
Don't use it when:
- There is only one variation and it doesn't combine with anything: a subclass or a parameter is simpler (KISS). The pattern earns its keep from the combinatorics onward.
- You need to remove or query layers frequently: the onion is opaque (there is no clean way to ask "does it have double cheese?" from outside) and unwrapping isn't part of the contract. If the order needs to list its extras for the kitchen, the right model may be data (a list of extras with pricing rules) rather than decorators — modeling with objects doesn't always beat modeling with data.
- The Component interface is huge: every decorator must implement it entirely, and twenty delegation methods to alter one is a bad sign (revisit ISP before Decorator).
- Identity matters: the decorated object has a different identity (
==,equals) than the original, which breaks caches and naive comparisons.
Relationship to other patterns (mentions only): Composite shares the interface and the philosophy — decorators and composites get along wonderfully: you can decorate a leaf of the tree; Proxy has an identical structure with an intent of access control rather than adding responsibilities (head-to-head in its lesson and in the comparison); Adapter also wraps but changes the interface; Strategy swaps the object's guts while Decorator changes its skin; and decorator stacks are conveniently assembled by the factories from module 2.
Common Mistakes and Tips
- Breaking the delegation. A decorator that forgets to call
wrapped.method()in some method cuts the chain: the inner layers silently stop executing. The abstract base that delegates by default (DishExtra) exists precisely for this. - Decorators with mutual knowledge. If
LargePortionExtradoesinstanceof DoubleCheeseExtrato adjust its math, layer independence is dead and the wrapping order becomes a minefield. Each decorator knows only the interface. - State shared with the wrapped object. A decorator that caches the wrapped object's
getPrice()goes stale if the dish changes. Stateless decorators (or ones with their own immutable state) sleep better. equals/hashCodeinherited cheerfully. Is a Margarita with cheeseequalsto a Margarita? Decide the semantics explicitly; by default, distinct identities, and documented.- The infinite onion in the debugger. Ten nested layers make debugging painful (ten frames for one
getPrice()). It is a real cost of the pattern: keep the stacks short and clearly named; if each layer'stoString()describes itself, the debugger becomes readable. - Tip: when a layer composition repeats (every production notifier carries logging + retries), don't scatter it around the code: give it a name in one single place — a factory, or the Supplier registry itself. Composing is cheap; composing the same way everywhere by hand is duplication in disguise.
Exercises
Exercise 1: the bacon extra and the house discount
Write (a) BaconExtra (+€1.20, description "... with bacon"); (b) HouseDiscount, a decorator that applies a percentage discount to the accumulated price and appends " [X% discount applied]" to the description. Does it matter where in the onion the discount is applied? Where would you place it, and why?
Exercise 2: reading the onion
Given this code, write the resulting description and price, justifying the order of the calculations:
Product p = new LargePortionExtra(
new DoubleCheeseExtra(
new Dish("Prosciutto", new BigDecimal("9.90"))));Exercise 3: decorator or not?
For each need, say whether Decorator is appropriate and, if not, what simple alternative you would use: (1) making all PideYa notifications, always, include the "[PideYa]" prefix; (2) measuring the duration of calls to PaymentGateway only in the performance-testing environment; (3) letting the kitchen list a dish's extras in order to prepare it.
Solutions
Solution 1:
public class BaconExtra extends DishExtra {
private static final BigDecimal SURCHARGE = new BigDecimal("1.20");
public BaconExtra(Product wrapped) { super(wrapped); }
@Override public String getDescription() { return wrapped.getDescription() + " with bacon"; }
@Override public BigDecimal getPrice() { return wrapped.getPrice().add(SURCHARGE); }
}
public class HouseDiscount extends DishExtra {
private final BigDecimal percentage; // e.g. 10 = 10%
public HouseDiscount(Product wrapped, BigDecimal percentage) {
super(wrapped);
this.percentage = percentage;
}
@Override public String getDescription() {
return wrapped.getDescription() + " [" + percentage + "% discount applied]";
}
@Override public BigDecimal getPrice() {
BigDecimal factor = BigDecimal.ONE.subtract(
percentage.movePointLeft(2));
return wrapped.getPrice().multiply(factor).setScale(2, RoundingMode.HALF_UP);
}
}Yes, it matters: the discount only discounts what it has inside. Applied in the middle of the onion, the outer extras would go undiscounted. It must be the outermost layer to discount the total — and since that rule is business, better to enforce it where composition happens (the factory/service that assembles the product), not entrust it to every programmer's memory.
Solution 2: from the inside out: base 9.90 → DoubleCheeseExtra: 9.90 + 1.50 = 11.40 → LargePortionExtra: 11.40 × 1.30 = €14.82. Description, also from the inside out: "Prosciutto" → "Prosciutto with double cheese" → "Prosciutto with double cheese, large portion". The large portion, being the outer layer, multiplies the cheese surcharge too — consistent with the menu's policy (a large portion carries more cheese).
Solution 3: (1) No: if it is always and for everyone, there is no combinatorics and no optionality; the prefix belongs in the common code (the base or the single sending point). Decorator for the invariable is ceremony. (2) Yes: a cross-cutting responsibility, optional (one environment only), without touching the gateways: a MetricsPaymentGateway implements PaymentGateway composed only in that environment — in fact it sits on the border with Proxy, as we will see. (3) No, not with pure decorators: the onion is opaque, and unwrapping it with instanceof is fighting the pattern. The kitchen needs structured data (a list of extras), a sign that this case calls for modeling the extras as data in addition to (or instead of) layers.
Conclusion
Decorator replaces the question "which subclass do I need for this combination?" with "which layers do I wrap today?": one common interface, one base object, and tiny decorators that add their responsibility and delegate the rest. PideYa now has composable extras over Product (DoubleCheeseExtra, GlutenFreeAdaptation, LargePortionExtra) and technical decorators over Notifier (RetryNotifier, LoggingNotifier), and along the way you have learned to read Java's I/O as what it always was: a textbook onion.
We have learned to translate interfaces, build bridges, assemble trees, and wrap layers. Meanwhile, in PideYa's checkout, the mobile app is still juggling: validate the cart, ask the market family for taxes, charge through the gateway, build the order, notify... five subsystems coordinated by hand from every client. Sometimes the best service a design can offer is not a clever structure but a simple door in front of the complexity. See you in Facade.
Software Design Patterns Course
Module 1: Introduction to Design Patterns
- What Are Design Patterns?
- History and Origin of Design Patterns
- Design Principles: SOLID and Other Foundations
- Essential UML for Understanding Patterns
- Classification of Design Patterns
- Advantages and Disadvantages of Using Design Patterns
Module 2: Creational Patterns
- Introduction to Creational Patterns
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparing and Choosing Creational Patterns
Module 3: Structural Patterns
- Introduction to Structural Patterns
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparing and Choosing Structural Patterns
Module 4: Behavioral Patterns
- Introduction to Behavioral Patterns
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparing and Choosing Behavioral Patterns
Module 5: Applying Design Patterns
- How to Select the Right Pattern
- Practical Examples of Pattern Usage
- Design Patterns in Real Projects
- Refactoring with Design Patterns
- Anti-Patterns: When Patterns Become a Problem
Module 6: Advanced Design Patterns
- Design Patterns in Modern Architectures
- Design Patterns in Microservices
- Design Patterns in Distributed Systems
- Concurrency Patterns
- Design Patterns in Agile Development
