LLD CASE STUDIES / OBJECT DESIGN BRIEF

Design a coffee machine

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).

IntermediatePhase 09 / Topic 5 of 10ResponsibilitiesCollaborationsExtensibility
01

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).

A barista with a recipe book

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.

02

When to use it

  • Common LLD question for Decorator and Builder.
  • Practicing inventory checks and recipes.
  • Extensible menus with add-ons.
03

Where it shows up in interviews

Customizable products

Recognize it when: base item plus combinable add-ons.

  • Design a coffee machine
  • Design a pizza ordering system
  • Design a burger builder
Resource-constrained production

Recognize it when: recipes consume limited stock.

  • Design a coffee machine
  • Design a restaurant kitchen
04

Where it is used in real software

Starbucks ordering

Base drinks with modifiers (sizes, shots, syrups, milk types) priced per add-on.

Smart coffee machines

Machines track bean, water, and milk levels and alert when refills are needed.

Point-of-sale modifiers

Restaurant POS systems model menu items with modifier groups.

05

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.
06

How it works, step by step

  1. 1
    Clarify

    Beverages, add-ons, payment, concurrency (single dispenser?), alerts.

  2. 2
    Entities

    Ingredient, Recipe, Beverage, AddOn, Order, IngredientStore, CoffeeMachine.

  3. 3
    Order building

    Beverage + add-ons -> total recipe and price.

  4. 4
    Check and consume atomically

    All ingredients or none.

  5. 5
    Brew and alert

    Template steps; notify observers on low stock.

07

Latte with extra shot and vanilla

Latte: 18 g coffee, 30 ml water, 150 ml milk, price 3.50

Step 1 / 4
ComponentCoffee (g)Milk (ml)Syrup (ml)Price
Latte1815003.50
+ extra shot+900+0.80
+ vanilla00+15+0.60
Total27150154.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.

08

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);
09

Complexity and performance

Price / recipeO(add-ons)

Decorator chain.

ConsumeO(ingredients)

Atomic under lock.

10

Trade-offs

Decorator vs list of add-ons

Decorators are elegant for arbitrary stacking; a simple list of AddOn objects on an Order is easier to serialize and display.

Hard-coded menu vs configuration

Recipes and prices in config let operators change the menu without code changes.

11

Variants and related techniques

Sizes

Size multipliers on recipes and prices.

Multiple dispensers

Concurrent brewing with shared ingredient store locks.

12

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.

13

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.

14

Practice problems

ProblemDifficultyWhat it trains
Coffee machine with add-ons and low-stock alertsMediumDecorator + Observer.
Support sizes and multiple dispensersHardConcurrency.