Decorator
Part of the free Low-Level Design course on LogicWiz, in the chapter: Structural patterns. The class-diagram exercises run in the app.
Decorator: gift wrap without a subclass for every combination
"I now have ItemWithGiftWrap, ItemWithEcoBag and ItemWithGiftWrapAndEcoBag. Marketing wants a note card too."
Arjun, counting subclasses on the whiteboard
The problem: one subclass per combination
For Diwali, SwiftKart lets customers add extras to any item in the cart:
- gift wrap: 30 rupees;
- eco bag (a reusable cloth bag): 10 rupees;
- note card with a printed message: 15 rupees.
Arjun reached for inheritance, the tool he knew best. Each extra became a subclass of CartItem. Then customers wanted two extras on one item, so he added a subclass for that pair too. Prices are in paise (one rupee is 100 paise).
public class CartItem {
protected final String name;
protected final long basePaise;
public CartItem(String name, long basePaise) {
this.name = name;
this.basePaise = basePaise;
}
public long pricePaise() {
return basePaise;
}
}
public class ItemWithGiftWrap extends CartItem {
public ItemWithGiftWrap(String name, long basePaise) { super(name, basePaise); }
public long pricePaise() { return basePaise + 3000; }
}
public class ItemWithGiftWrapAndEcoBag extends CartItem {
public ItemWithGiftWrapAndEcoBag(String name, long basePaise) { super(name, basePaise); }
public long pricePaise() { return basePaise + 3000 + 1000; }
}
Count what this costs:
- The classes multiply. Each extra is either on or off, so three extras give 2 × 2 × 2 = 8 combinations. Leave out the plain item, and that is 7 subclasses. Four extras need 15, and five need 31.
- Prices are copied. The 3000 for gift wrap appears in every class that includes gift wrap. When the price changes, Arjun edits four classes and hopes he finds them all.
- The choice happens at the wrong time. A subclass is fixed when the object is created. But a customer adds gift wrap after the chocolates are in the cart. With subclasses, you throw the
CartItemaway and create a new object of a different class.
The root problem is this. Inheritance fixes behaviour when you write the code. Here, the customer chooses the behaviour while the app runs.
The idea: wrapping a present
Think of a box of chocolates you are giving a friend. You wrap it in paper. It is still a present you can hand over. You put the wrapped box in a cloth bag. It is still a present. You tie a card to the bag. Still a present. Each layer adds something, and nothing inside changes.
The code version is an object that looks like an item from the outside, holds the item inside, and adds its own part. Ask the outer layer for the price. It asks the layer inside for its price, adds its own 30 rupees and returns the total. Each layer knows only the one directly inside it.
This is the decorator pattern. A decorator is an object that implements the same interface as the object it wraps. It holds a reference to that object, passes each call on to it, and adds behaviour before or after. The roles have names:
- The component is the shared interface:
Priced. - The concrete component is the plain object at the centre:
CartItem. - A decorator implements the component and holds one component inside:
GiftWrap,EcoBag,NoteCard.
A decorator is a component, so you can wrap a decorator in another decorator. That is how layers stack.
The present stops being a good picture in two places. Real paper hides what is inside, but a decorator hides nothing: every call goes through to the centre. And with a real present, the order of layers is only about looks. In code, the order of layers can change the answer, as you will see below.
The structure
classDiagram
class Priced {
<<interface>>
+pricePaise() long
+describe() String
}
Priced <|.. CartItem
Priced <|.. AddOn
AddOn --> Priced : wraps
AddOn <|-- GiftWrap
AddOn <|-- EcoBag
AddOn <|-- NoteCard
AddOn is an abstract class that holds the wrapped object, so each add-on does not repeat that field. Look at the two arrows between AddOn and Priced. The dashed one says an add-on is a Priced. The solid one says an add-on has a Priced inside. The pattern needs both.
Build it step by step
Step 1: the shared interface, and the plain item.
public interface Priced {
long pricePaise();
String describe();
}
public class CartItem implements Priced {
private final String name;
private final long basePaise;
public CartItem(String name, long basePaise) {
this.name = name;
this.basePaise = basePaise;
}
public long pricePaise() { return basePaise; }
public String describe() { return name; }
}
Step 2: the base decorator, and one add-on.
public abstract class AddOn implements Priced {
protected final Priced inner; // the object this layer wraps
protected AddOn(Priced inner) {
this.inner = inner;
}
}
public class GiftWrap extends AddOn {
public GiftWrap(Priced inner) {
super(inner);
}
public long pricePaise() { return inner.pricePaise() + 3000; }
public String describe() { return inner.describe() + " + gift wrap"; }
}
GiftWrap does not know what it wraps. It may be a plain item, or an item already in an eco bag. It only knows that inner can answer pricePaise(). EcoBag and NoteCard are the same few lines with their own price and label.
Step 3: build the stack while the app runs.
Priced item = new CartItem("Dark chocolate box", 45000);
item = new GiftWrap(item); // customer taps "Add gift wrap"
item = new NoteCard(item); // customer taps "Add a note card"
long total = item.pricePaise(); // 45000 + 3000 + 1500 = 49500
String label = item.describe(); // "Dark chocolate box + gift wrap + note card"
Follow the call. NoteCard.pricePaise() asks GiftWrap, which asks CartItem. CartItem returns 45000. GiftWrap adds 3000 and returns 48000. NoteCard adds 1500 and returns 49500.
The order of wrapping. With fixed prices, order does not change the total, only the label. It matters as soon as one layer works on the result of the layers inside. Suppose marketing adds a FestiveDiscount decorator: 10% off whatever it wraps.
new FestiveDiscount(new GiftWrap(chocolates))takes 10% off 48000, so the customer pays 43200. The discount covers the wrap.new GiftWrap(new FestiveDiscount(chocolates))takes 10% off 45000, then adds 3000, so the customer pays 43500.
Both are valid. The business must choose one, and the code that builds the stack must follow that choice every time.
During a mock interview, a candidate said, "Decorator is inheritance with extra steps." It is the opposite. The behaviour comes from composition: each layer holds another object and calls it. The extends AddOn only gives the add-ons a shared type and a shared field. That is why you can add or skip layers for each object at runtime, which inheritance cannot do.
You have used decorators if you have read a file in Java. new BufferedReader(new FileReader(path)) wraps a file reader in a layer that adds buffering. Both objects are Readers.
Decorator, inheritance or a list of add-ons
Three designs can hold these extras. Choose by asking what each extra does.
- Inheritance fits a few fixed variants, chosen when the object is created, that never combine. A
FrozenItemthat always needs a cold bag is a fair subclass. - A list of add-ons fits when each extra only adds a price and a label. Give each cart line a
List<Extra>, whereExtrais a small record holding a name and a price. The total is the base price plus the sum of the list. Removing one extra means deleting it from the list. Each shows on the invoice as its own line, and the list saves to the database as rows. - A decorator fits when each layer changes behaviour, not only a number, or when the order of layers matters. A discount that works on the inner total is one case. Layers that add logging or retries around a service are another.
For gift wrap, eco bag and note card alone, Priya's honest answer was the list. All three add a fixed price, and finance wants each as a separate invoice line. Her rule: start with the list, and move to decorators when the first layer arrives that works on the inner result, such as the festive discount.
Decorators have costs:
- Order matters, and nothing in the type system checks it.
- Identity changes. After wrapping,
item instanceof CartItemis false, because the outer object is aNoteCard. Code that checks types breaks. - Removing a middle layer is awkward. To take gift wrap out of the middle of a stack, you rebuild the stack.
- Debugging is deeper. One call passes through every layer, so stack traces grow.
Decorators are common around services. A LoggingPaymentGateway wraps any PaymentGateway and logs each charge. A RetryingPaymentGateway retries timeouts. Each does one job, and main stacks the ones you need.
Two patterns look similar. An adapter changes the interface, while a decorator keeps it. A proxy, the last lesson of this chapter, keeps it too, but for a different purpose. In interview problems, decorators appear as toppings on a pizza or coffee order, as extra rules on a parking fee, and as retry or logging layers in a notification service.
Try it: draw a decorator chain for add-ons
The starter has a subclass of CartItem for each combination of extras. Replace them with one interface and decorators that each wrap anything priced, so layers stack on one item in any order.
📌 Summary: A decorator implements the same interface as the object it wraps, holds it, and adds to each call. Layers stack at runtime, and their order can matter. For fixed prices only, a list is simpler.
What Arjun learns next
Decorators wrap one object. Next, marketing creates a breakfast bundle that holds another bundle inside it, and Arjun's price code fills with type checks. The fix is the composite pattern.
Check your understanding
- SwiftKart offers four extras, and a customer can pick any combination. How many classes does each design need for the extras?
- Chocolates cost 450 rupees, gift wrap adds 30, and FestiveDiscount takes 10% off what it wraps. Which stack makes the customer pay 432 rupees?
- Every extra only adds a fixed price, and finance wants each extra as its own line on the invoice. What fits best?
- After the customer adds a note card, `item instanceof CartItem` returns false. Why?