Overview
Design a coffee machine that makes beverages (espresso, latte, cappuccino) from ingredients (water, milk, coffee beans, sugar) with optional add-ons (extra shot, syrup, oat milk). Requirements: show available beverages based on ingredient levels, compute price including add-ons, check and deduct ingredients atomically, brew following a sequence of steps, and alert when ingredients are low.
The design highlights Decorator (or a list of add-ons) for customizations and pricing, Builder for orders, a Recipe (ingredient quantities) per beverage, Template Method for the brewing process (grind, brew, add milk, dispense), and Observer for low-ingredient alerts. The machine itself can be a small state machine (ready, brewing, needs refill).
The barista checks the recipe (18 g beans, 150 ml milk), confirms there is enough in stock, prepares the drink in the usual order, and adds your extras. When the milk is nearly gone, they tell the manager.
When to use it
- Common LLD question for Decorator and Builder.
- Practicing inventory checks and recipes.
- Extensible menus with add-ons.
Where it shows up in interviews
Recognize it when: base item plus combinable add-ons.
- Design a coffee machine
- Design a pizza ordering system
- Design a burger builder
Recognize it when: recipes consume limited stock.
- Design a coffee machine
- Design a restaurant kitchen
Where it is used in real software
Base drinks with modifiers (sizes, shots, syrups, milk types) priced per add-on.
Machines track bean, water, and milk levels and alert when refills are needed.
Restaurant POS systems model menu items with modifier groups.
Key terms
- Recipe
- Ingredient quantities for a beverage.
- Add-on
- Extra that changes price and ingredients.
- Ingredient store
- Current stock with atomic reserve/consume.
- Low-stock threshold
- Level that triggers alerts.
How it works, step by step
- 1Clarify
Beverages, add-ons, payment, concurrency (single dispenser?), alerts.
- 2Entities
Ingredient, Recipe, Beverage, AddOn, Order, IngredientStore, CoffeeMachine.
- 3Order building
Beverage + add-ons -> total recipe and price.
- 4Check and consume atomically
All ingredients or none.
- 5Brew and alert
Template steps; notify observers on low stock.
Latte with extra shot and vanilla
Latte: 18 g coffee, 30 ml water, 150 ml milk, price 3.50
| Component | Coffee (g) | Milk (ml) | Syrup (ml) | Price |
|---|---|---|---|---|
| Latte | 18 | 150 | 0 | 3.50 |
| + extra shot | +9 | 0 | 0 | +0.80 |
| + vanilla | 0 | 0 | +15 | +0.60 |
| Total | 27 | 150 | 15 | 4.90 |
NOWComponent: Latte | Coffee (g): 18 | Milk (ml): 150 | Syrup (ml): 0 | Price: 3.50
Add-ons combine both price and ingredient requirements; the store checks the total before brewing.
Implementation
type Ingredient = "coffee" | "water" | "milk" | "syrup" | "sugar";type Recipe = Partial<Record<Ingredient, number>>; interface Drink { name(): string; price(): number; recipe(): Recipe } const base = (name: string, price: number, recipe: Recipe): Drink => ({ name: () => name, price: () => price, recipe: () => recipe });const menu = { espresso: base("Espresso", 250, { coffee: 18, water: 30 }), latte: base("Latte", 350, { coffee: 18, water: 30, milk: 150 }), cappuccino: base("Cappuccino", 330, { coffee: 18, water: 30, milk: 100 }),}; // Decorator: add-ons wrap drinks and add price and ingredientsconst merge = (a: Recipe, b: Recipe): Recipe => { const out: Recipe = { ...a }; for (const [k, v] of Object.entries(b) as [Ingredient, number][]) out[k] = (out[k] ?? 0) + v; return out;};const addOn = (label: string, price: number, extra: Recipe) => (d: Drink): Drink => ({ name: () => `${d.name()} + ${label}`, price: () => d.price() + price, recipe: () => merge(d.recipe(), extra),});const extraShot = addOn("extra shot", 80, { coffee: 9, water: 15 });const vanilla = addOn("vanilla", 60, { syrup: 15 }); class IngredientStore { constructor(private stock: Record<Ingredient, number>, private lowAt: Record<Ingredient, number>, private onLow: (i: Ingredient, left: number) => void) {} canMake(r: Recipe) { return (Object.entries(r) as [Ingredient, number][]).every(([k, v]) => this.stock[k] >= v); } consume(r: Recipe) { if (!this.canMake(r)) throw new Error("Insufficient ingredients"); for (const [k, v] of Object.entries(r) as [Ingredient, number][]) { this.stock[k] -= v; if (this.stock[k] <= this.lowAt[k]) this.onLow(k, this.stock[k]); // observer } }} class CoffeeMachine { private busy = false; constructor(private store: IngredientStore) {} available() { return Object.values(menu).filter((d) => this.store.canMake(d.recipe())).map((d) => d.name()); } brew(drink: Drink) { if (this.busy) throw new Error("Machine busy"); this.busy = true; try { this.store.consume(drink.recipe()); // atomic check + consume return [`grind`, `brew ${drink.name()}`, `dispense`].join(" -> "); } finally { this.busy = false; } }} const machine = new CoffeeMachine(new IngredientStore( { coffee: 500, water: 2000, milk: 1000, syrup: 200, sugar: 300 }, { coffee: 50, water: 200, milk: 150, syrup: 30, sugar: 30 }, (i, left) => console.log(`Low ${i}: ${left}`),));const order = vanilla(extraShot(menu.latte));order.price(); // 490machine.brew(order);Complexity and performance
Decorator chain.
Atomic under lock.
Trade-offs
Decorators are elegant for arbitrary stacking; a simple list of AddOn objects on an Order is easier to serialize and display.
Recipes and prices in config let operators change the menu without code changes.
Variants and related techniques
Size multipliers on recipes and prices.
Concurrent brewing with shared ingredient store locks.
Common mistakes
- Checking ingredients then consuming separately without locking.
Fix: Check and consume atomically.
- A subclass per drink and add-on combination.
Fix: Use decorators or add-on lists.
Interview questions
How would you add a new add-on like oat milk?
Create one new decorator (or AddOn entry) that replaces milk with oat milk in the recipe and adjusts price; no changes to existing drinks or the machine.
How do you prevent brewing with insufficient ingredients?
Compute the full recipe including add-ons, check and deduct all ingredients in one synchronized operation, and fail before brewing if any is short.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Coffee machine with add-ons and low-stock alerts | Medium | Decorator + Observer. |
| Support sizes and multiple dispensers | Hard | Concurrency. |