Coffee Shop Ordering
Wrap a base drink with toppings, and each one adds its own cost and description. The Decorator pattern means you never modify the original drink class when adding new toppings.
Key Abstractions
Component interface with cost() and description() that all drinks and decorators implement
Abstract decorator wrapping a Beverage, delegating cost and description to the wrapped drink
Strategy interface for applying discounts like happy hour or loyalty pricing
Collection of beverages with a pricing strategy applied to calculate the final total
Class Diagram
The Key Insight
A coffee shop menu looks simple until you realize customers combine things freely. Espresso with milk. Latte with caramel and whip cream. Tea with double sugar. If you try to model every combination as its own class, you end up with dozens of them. And every new topping doubles the count.
The Decorator pattern sidesteps this entirely. A drink is an object. A topping is also an object that wraps a drink. Both share the same interface. So CaramelDecorator(MilkDecorator(Latte())) is just three objects nested inside each other, each calling cost() on the thing it wraps and adding its own price. You get infinite combinations from a handful of small classes.
Pricing is a separate problem. The drinks know their own costs, but they should not know about happy hour promotions or loyalty discounts. That business logic belongs at the order level, behind a Strategy interface. Swap the strategy object and the discount changes. No conditionals, no flags.
Requirements
Functional
- Create base beverages (Espresso, Latte, Tea) each with a fixed price
- Add any combination of toppings (Milk, Sugar, Whip Cream, Caramel) to any drink
- Toppings stack. A drink can have multiple toppings, including duplicates
- An order collects multiple beverages and computes a total
- Support different pricing strategies (standard, happy hour, loyalty) applied at checkout
Non-Functional
- Adding a new topping should require only one new class, no changes to existing code
- Adding a new pricing strategy should require only one new class
- Beverage cost calculation should work regardless of how many decorators are stacked
- The system should follow the Open/Closed Principle throughout
Design Decisions
Why Decorator over subclassing?
If you have 3 base drinks and 4 toppings, subclassing gives you 3 * 2^4 = 48 classes. EspressoWithMilk, EspressoWithMilkAndSugar, EspressoWithMilkAndSugarAndWhipCream, and so on. Adding a fifth topping doubles that to 96. Decorator gives you 3 + 4 = 7 classes. A fifth topping adds exactly 1 more class. The math alone makes the decision obvious.
Why Strategy for pricing instead of discount flags?
A boolean isHappyHour on the Order works for one discount type. But what happens when you add loyalty discounts, seasonal promos, and combo deals? Each needs different calculation logic and you end up with a growing pile of if-else branches. Strategy encapsulates each algorithm in its own class. You swap the strategy object instead of editing conditional logic. When the marketing team invents a new promotion next month, you write one class and plug it in.
Why does each decorator hold a reference to Beverage, not to the specific concrete drink?
This is what makes decoration composable. MilkDecorator wraps "any Beverage", which means it can wrap an Espresso, a Latte, or another decorator like SugarDecorator. If it specifically held a reference to Espresso, you could not stack decorators on top of each other. The abstraction is the whole point.
Why is Order separate from Beverage?
A drink knows its own cost. That is all it should know. It has no business understanding pricing strategies, order totals, or discount rules. The Order aggregates drinks and applies business rules on top. Mixing these concerns into the Beverage class would mean every drink needs to understand every pricing policy. Separation of concerns keeps both sides simple.
Interview Follow-ups
- "How would you handle size variants like Small, Medium, Large?" Add a size parameter to the base Beverage constructor that adjusts the base price. The decorators stay unchanged because they add a fixed amount on top of whatever
cost()returns from the wrapped beverage. - "What if some toppings are incompatible with certain drinks?" Introduce a validation step in the decorator constructor or use a Builder that checks compatibility before wrapping. Keep the decorator chain itself clean and push validation to construction time.
- "How would you add tax calculation?" Tax is another order-level concern, similar to pricing strategy. You could add a TaxCalculator that the Order applies after the pricing strategy, or compose it as part of the strategy itself. Either way, beverages stay unaware.
- "What about combo deals where ordering specific items together triggers a discount?" That logic lives in a new PricingStrategy implementation. ComboStrategy inspects the items in the order, detects qualifying combos, and applies the appropriate discount. Individual beverages never change.
Code Implementation
1 from __future__ import annotations
2 from abc import ABC, abstractmethod
3
4
5 # ---------- Component interface ----------
6
7 class Beverage(ABC):
8 """Base component. Every drink and every decorator is a Beverage."""
9
10 @abstractmethod
11 def cost(self) -> float: ...
12
13 @abstractmethod
14 def description(self) -> str: ...
15
16
17 # ---------- Concrete components ----------
18
19 class Espresso(Beverage):
20 def cost(self) -> float:
21 return 3.00
22
23 def description(self) -> str:
24 return "Espresso"
25
26
27 class Latte(Beverage):
28 def cost(self) -> float:
29 return 4.50
30
31 def description(self) -> str:
32 return "Latte"
33
34
35 class Tea(Beverage):
36 def cost(self) -> float:
37 return 2.50
38
39 def description(self) -> str:
40 return "Tea"
41
42
43 # ---------- Abstract decorator ----------
44
45 class ToppingDecorator(Beverage, ABC):
46 """Wraps any Beverage. Subclasses add their own cost and description."""
47
48 def __init__(self, beverage: Beverage):
49 self._beverage = beverage
50
51
52 # ---------- Concrete decorators ----------
53
54 class MilkDecorator(ToppingDecorator):
55 def cost(self) -> float:
56 return self._beverage.cost() + 0.60
57
58 def description(self) -> str:
59 return self._beverage.description() + ", Milk"
60
61
62 class SugarDecorator(ToppingDecorator):
63 def cost(self) -> float:
64 return self._beverage.cost() + 0.30
65
66 def description(self) -> str:
67 return self._beverage.description() + ", Sugar"
68
69
70 class WhipCreamDecorator(ToppingDecorator):
71 def cost(self) -> float:
72 return self._beverage.cost() + 0.75
73
74 def description(self) -> str:
75 return self._beverage.description() + ", Whip Cream"
76
77
78 class CaramelDecorator(ToppingDecorator):
79 def cost(self) -> float:
80 return self._beverage.cost() + 0.90
81
82 def description(self) -> str:
83 return self._beverage.description() + ", Caramel"
84
85
86 # ---------- Strategy interface ----------
87
88 class PricingStrategy(ABC):
89 """Encapsulates a pricing algorithm. Swap strategies without touching drinks."""
90
91 @abstractmethod
92 def apply(self, cost: float) -> float: ...
93
94 @abstractmethod
95 def label(self) -> str: ...
96
97
98 class StandardPricing(PricingStrategy):
99 def apply(self, cost: float) -> float:
100 return cost
101
102 def label(self) -> str:
103 return "Standard Pricing"
104
105
106 class HappyHourPricing(PricingStrategy):
107 def apply(self, cost: float) -> float:
108 return cost * 0.80
109
110 def label(self) -> str:
111 return "Happy Hour (20% off)"
112
113
114 class LoyaltyPricing(PricingStrategy):
115 def apply(self, cost: float) -> float:
116 return cost * 0.85
117
118 def label(self) -> str:
119 return "Loyalty Member (15% off)"
120
121
122 # ---------- Order ----------
123
124 class Order:
125 """Aggregates beverages and applies a pricing strategy to the total."""
126
127 def __init__(self, strategy: PricingStrategy | None = None):
128 self._items: list[Beverage] = []
129 self._strategy = strategy or StandardPricing()
130
131 def add_item(self, beverage: Beverage) -> None:
132 self._items.append(beverage)
133
134 def set_strategy(self, strategy: PricingStrategy) -> None:
135 self._strategy = strategy
136
137 def subtotal(self) -> float:
138 return sum(item.cost() for item in self._items)
139
140 def total(self) -> float:
141 return self._strategy.apply(self.subtotal())
142
143
144 if __name__ == "__main__":
145 # 1. Plain espresso
146 espresso = Espresso()
147 print(f"{espresso.description()}: ${espresso.cost():.2f}")
148
149 # 2. Latte with milk and caramel (stacked decorators)
150 latte = CaramelDecorator(MilkDecorator(Latte()))
151 print(f"{latte.description()}: ${latte.cost():.2f}")
152
153 # 3. Tea with sugar and whip cream
154 tea = WhipCreamDecorator(SugarDecorator(Tea()))
155 print(f"{tea.description()}: ${tea.cost():.2f}")
156
157 # 4. Order with standard pricing
158 order = Order()
159 order.add_item(espresso)
160 order.add_item(latte)
161 order.add_item(tea)
162 print(f"\nSubtotal: ${order.subtotal():.2f}")
163 print(f"Standard total: ${order.total():.2f}")
164
165 # 5. Happy hour pricing
166 order.set_strategy(HappyHourPricing())
167 print(f"Happy hour total: ${order.total():.2f}")
168
169 # 6. Loyalty pricing
170 order.set_strategy(LoyaltyPricing())
171 print(f"Loyalty total: ${order.total():.2f}")Common Mistakes
- ✗Making Beverage a concrete class instead of an interface/abstract class. Decorators need a common type to wrap.
- ✗Putting topping logic inside the Beverage class with flags like hasWhipCream, hasMilk. That is a combinatorial explosion.
- ✗Forgetting that decorator order can matter. Double milk charges twice because you have two MilkDecorators in the chain.
- ✗Coupling pricing strategy to the beverage. Pricing is an order-level concern, not a drink-level one.
Key Points
- ✓Decorators compose at runtime. A Latte with milk and caramel is three objects wrapping each other.
- ✓New toppings never touch existing code. Write a new decorator class and you are done.
- ✓Strategy swaps pricing logic without changing how beverages calculate their base cost.
- ✓cost() recurses through the decorator chain. Each layer adds its price to the inner beverage's price.