Design a Coffee Machine
Recipes, shared ingredients, and concurrent dispensing — a compact study in Builder + resource contention.
Problem Statement
Design the object model for a coffee machine that prepares recipe-based beverages from a shared ingredient inventory. Customers select drinks such as espresso, latte, and cappuccino. Each drink has a fixed recipe of water, milk, coffee, and sugar. The machine must check stock, deduct all required ingredients atomically, brew the drink, and support refilling when stock runs low. The focus is the class model and concurrency boundary: immutable recipes, a central beverage factory, one shared machine plus inventory per physical device, and safe dispensing when multiple outlets are active.
Business context
Coffee Machine is a compact real-world LLD interview problem used by Amazon, Swiggy, and Oracle because it combines everyday domain modelling with resource contention. A strong solution separates what a drink is from how the machine prepares it. Recipes are immutable, inventory owns stock mutation, the factory owns supported beverages, and the machine coordinates outlet capacity. Interviewers look for atomic check-and-deduct logic more than for payment screens or hardware details.
Functional Requirements
Support named beverages such as espresso, latte, and cappuccino.
Represent every beverage as a recipe of ingredient quantities.
Maintain inventory for water, milk, coffee, and sugar.
Before dispensing, verify all required ingredients are available.
Deduct all ingredients atomically only when the full recipe can be satisfied.
Allow multiple outlets to prepare drinks concurrently up to machine capacity.
Expose refill operations and inventory snapshots so operators can detect low-stock ingredients.
Report unsupported beverages and insufficient ingredients clearly.
Non-Functional Requirements
Inventory correctness
No request may partially consume ingredients. The check and deduction must be one critical section inside the inventory.
Concurrency safety
Multiple outlets may brew at the same time, but stock mutation must remain serialized so two requests cannot spend the same units.
Extensibility
Adding a new beverage should be a recipe and factory change, not a rewrite of the preparation workflow.
Operator visibility
The design should expose available quantities and snapshots so low-stock alerts and refill dashboards can be layered on top.
Testability
Recipes, inventory, and machine coordination should be testable without real hardware or background services.
Requirement Clarification
QAre ingredient units grams, milliliters, or packets?
Use positive integer units and keep the unit convention outside the model. The inventory only needs consistent quantities per ingredient.
QCan an outlet reserve ingredients and brew later?
For the base design, preparation consumes ingredients immediately and then brews. Reservation queues are an extension.
QDoes outlet count mean parallel brewing or separate machines?
It means one physical machine with multiple dispensing outlets. A semaphore limits how many preparations can be active at once.
QWhat should happen when stock is low but still enough for the current drink?
Prepare the drink successfully, then let an operator-facing low-stock check decide whether to trigger refill alerts.
QDo we need payment, cups, sensors, or cleaning cycles?
Not in the core model. Those are boundary adapters or follow-up extensions around the preparation flow.
UML Class Diagram
Sequence Diagram
Entity Identification
CoffeeMachine
Aggregate coordinator for one physical machine. It acquires an outlet permit, asks the factory for a beverage, consumes inventory, brews, and releases the permit.
IngredientInventory
Single owner of stock quantities. Refill, consume, available, and snapshot are synchronized so inventory state cannot be corrupted by concurrent outlets.
Ingredient
Enum vocabulary shared by recipes and inventory: WATER, MILK, COFFEE, and SUGAR.
Recipe
Immutable mapping from ingredient to quantity. It protects callers from changing a beverage after the factory creates it.
Recipe.Builder
Readable recipe construction API. It validates positive quantities, combines repeated ingredients, and produces an immutable Recipe.
Beverage
Value object holding a drink name and its recipe. The machine prepares the beverage without knowing the recipe details.
BeverageFactory
Creation boundary for supported drink names. It maps input such as latte to the correct Beverage and recipe.
InsufficientIngredientException
Domain error carrying the ingredient that failed, the required quantity, and the available quantity for clear outlet feedback.
Design Patterns Used
Recipe.Builder makes drink recipes readable and immutable. The factory can express latte or cappuccino quantities without exposing mutable maps.
BeverageFactory.create centralizes supported beverage names and constructs the matching Beverage. The machine depends on creation behavior, not on drink-specific conditionals in the preparation flow.
A physical device should expose one shared CoffeeMachine and one shared IngredientInventory to all outlets. The reference classes remain constructor-injected for tests, while production composition can publish a single instance per device.
Step-by-Step Design
1Use one ingredient vocabulary
Start with the Ingredient enum so recipes, inventory, exceptions, and low-stock checks all speak the same language.
2Build immutable recipes
Represent a beverage as a Recipe and create it through Recipe.Builder. Once built, the map is unmodifiable, so concurrent requests cannot alter recipe quantities.
public Recipe latteRecipe() { return Recipe.builder() .add(Ingredient.WATER, 120) .add(Ingredient.MILK, 80) .add(Ingredient.COFFEE, 18) .add(Ingredient.SUGAR, 5) .build(); }3Centralize beverage creation
BeverageFactory owns the mapping from customer input to Beverage. Adding mocha or tea changes the factory recipes, while CoffeeMachine.prepare stays stable.
4Make inventory consumption atomic
IngredientInventory.consume first checks every required ingredient, then deducts every ingredient. Keeping both loops synchronized prevents partial consumption and double spending.
public synchronized void consume(Map<Ingredient, Integer> required) { for (Map.Entry<Ingredient, Integer> entry : required.entrySet()) { Ingredient ingredient = entry.getKey(); int need = entry.getValue(); int have = stock.get(ingredient); if (have < need) { throw new InsufficientIngredientException(ingredient, need, have); } } for (Map.Entry<Ingredient, Integer> entry : required.entrySet()) { stock.put(entry.getKey(), stock.get(entry.getKey()) - entry.getValue()); } }5Limit concurrent dispensing with outlet permits
CoffeeMachine.prepare acquires a semaphore permit before brewing and releases it in a finally block. This models finite outlets without weakening inventory correctness.
6Expose one machine and inventory per device
In production, a holder or dependency injection container should publish a single CoffeeMachine wired to a single IngredientInventory for each physical machine.
public final class CoffeeMachineHolder { private static final IngredientInventory INVENTORY = new IngredientInventory(); private static final CoffeeMachine INSTANCE = new CoffeeMachine(2, INVENTORY, new BeverageFactory()); private CoffeeMachineHolder() { } public static CoffeeMachine machine() { return INSTANCE; } }7Use snapshot and refill for low-stock operations
The machine exposes refill, and inventory exposes available plus snapshot. A dashboard can compare the snapshot with thresholds and call refill without entering the brew path.
Complete Java Implementation
Explanation of Every Class
Ingredient
Enum listing WATER, MILK, COFFEE, and SUGAR. It is the shared key type for recipes, stock, snapshots, and insufficient-stock errors.
InsufficientIngredientException
Runtime domain exception raised when inventory cannot satisfy a recipe. It carries the ingredient, required quantity, and available quantity so the outlet can show a precise failure.
Recipe
Immutable value object wrapping an unmodifiable EnumMap from Ingredient to quantity. Callers can read requirements but cannot mutate a finished recipe.
Recipe.Builder
Nested builder that validates positive quantities, merges repeated ingredient additions, rejects empty recipes, and returns an immutable Recipe.
Beverage
Final value object containing the display name and Recipe. It keeps the drink identity and ingredient requirements together for the machine workflow.
BeverageFactory
Factory that normalizes the requested beverage name and returns Espresso, Latte, or Cappuccino with the correct recipe. Unsupported names fail at the creation boundary.
IngredientInventory
Synchronized stock store backed by an EnumMap. Refill validates positive quantities, consume performs all checks before any deduction, and snapshot returns an unmodifiable copy for low-stock checks.
CoffeeMachine
Coordinator that owns the shared inventory, factory, and outlet semaphore. Prepare acquires a permit, creates the beverage, consumes ingredients, brews, and always releases the permit.
Dry Run
Sample input
Machine with 2 outlets. Initial stock: WATER 300, MILK 150, COFFEE 60, SUGAR 20. Requests arrive for Latte, Espresso, Cappuccino, then Espresso again. Low-stock coffee threshold is 25.
| Step | Action | Outlet state | Inventory snapshot | Result |
|---|---|---|---|---|
| 1 | Refill initial stock | 2 permits free | W300 M150 C60 S20 | Inventory ready |
| 2 | Prepare Latte | 1 permit held during brew | W180 M70 C42 S15 | Latte prepared |
| 3 | Prepare Espresso while another outlet may be active | Second permit can be held | W120 M70 C24 S15 | Espresso prepared after atomic consume |
| 4 | Check low-stock threshold for coffee | 2 permits free after brews | Coffee 24 | Refill recommended |
| 5 | Prepare Cappuccino | 1 permit held during brew | W20 M10 C4 S9 | Cappuccino prepared |
| 6 | Prepare Espresso again | Permit released after failure | W20 M10 C4 S9 | Fails with insufficient WATER: required 60, available 20 |
The important invariant is visible in steps 5 and 6: stock changes only after a full recipe passes validation. The failed espresso does not reduce coffee or any other ingredient.
Complexity Analysis
| Operation | Time | Space | Note |
|---|---|---|---|
| prepare | O(I) | O(1) | I is the number of ingredient types in the recipe. Semaphore acquire and release are constant-time coordination around the stock check. |
| consume ingredients | O(I) | O(1) | Inventory scans required ingredients once to validate and once to deduct. |
| refill one ingredient | O(1) | O(1) | A single synchronized EnumMap update. |
| snapshot or low-stock scan | O(N) | O(N) | N is total ingredient types. Snapshot copies the inventory so callers cannot mutate internal state. |
| create beverage | O(I) | O(I) | The factory builds a recipe map for the selected drink. |
With four ingredients, all operations are effectively constant in practice. The design still states O(I) because new machines may add syrups, powders, cup sizes, or toppings.
Extensibility
Add a new beverage
Add a recipe method and name branch in BeverageFactory. CoffeeMachine.prepare and IngredientInventory.consume remain unchanged.
Add a new ingredient
Add an Ingredient enum value and initialize its stock. Recipes can then include it through Recipe.Builder.
Low-stock policy
Layer a threshold map over IngredientInventory.snapshot to alert when any quantity falls below its configured minimum.
Multiple physical machines
Create one CoffeeMachine plus IngredientInventory pair per device id. Each pair is a singleton for that device, not a global singleton across the fleet.
Async brew hardware
Replace the private brew method with a hardware adapter. Keep recipe validation and inventory mutation before hardware activation.
Alternative Designs
Preloaded recipe catalog
Build all recipes once into an immutable map from beverage name to Beverage instead of rebuilding a Recipe on every factory call.
Tradeoffs
Faster create calls and fewer allocations, but less flexible if operators edit recipes at runtime.
Command per beverage request
Represent each preparation as a command that can be queued, retried, cancelled, or audited before reaching the machine.
Tradeoffs
Useful for kiosks with mobile ordering, but overkill for a simple synchronous machine interview design.
Per-ingredient locks
Lock only the ingredients required by a recipe instead of synchronizing the whole inventory.
Tradeoffs
May improve throughput with many ingredients, but introduces lock ordering risks and is unnecessary for four core ingredients.
Static singleton classes
Make CoffeeMachine and IngredientInventory expose getInstance methods directly.
Tradeoffs
Simple to explain, but it hides dependencies and makes tests share state. Constructor injection plus singleton composition is safer.
Common Mistakes
- ×
Checking ingredient availability in one method and deducting in another, which creates a race between outlets.
- ×
Deducting ingredients as they are checked, causing partial consumption when a later ingredient is missing.
- ×
Putting every beverage recipe inside CoffeeMachine.prepare instead of a factory or catalog.
- ×
Returning the mutable inventory map directly from snapshot and letting callers corrupt stock.
- ×
Treating outlet concurrency as a replacement for inventory locking. They solve different problems.
- ×
Using a global static singleton in tests and accidentally leaking stock between test cases.
- ×
Ignoring unsupported beverage names and returning null instead of failing clearly.
Follow-up Interview Questions
QHow would you add mocha without changing the machine workflow?
Add chocolate or syrup as an Ingredient if needed, then add a mocha recipe in BeverageFactory. The prepare flow still creates, consumes, brews, and releases the permit.
QWhy is consume synchronized if the machine already uses a semaphore?
The semaphore limits active outlets, but two active outlets can still reach inventory together. The synchronized consume method protects the check-and-deduct critical section.
QHow do you notify operators about low stock?
Read IngredientInventory.snapshot, compare each quantity to a threshold map, and emit alerts outside the synchronized brew path.
QShould CoffeeMachine be a hard-coded static singleton?
Usually no. Treat it as a singleton at the composition boundary so production has one instance per device while tests can create isolated instances.
QWhat if brewing succeeds but hardware later fails?
Add a hardware adapter and an operation log. Decide whether ingredients are refundable based on physical reality, but keep the domain event auditable.
Production Considerations
Durable inventory
Persist stock adjustments with an audit log so a power loss does not reset ingredient counts or hide failed dispense attempts.
Fair outlet scheduling
If requests queue, make semaphore acquisition fair or put requests through an explicit FIFO queue to avoid starvation.
Low-stock observability
Emit gauges for each ingredient, alerts for threshold breaches, and counters for insufficient-stock failures by beverage.
Recipe governance
Keep recipe changes versioned. A prepared drink should be traceable to the recipe quantities active at preparation time.
Hardware idempotency
Dispense commands to pumps and grinders should include operation ids so retries do not double-brew a drink.
What Interviewers Look For
Did you identify inventory check-and-deduct as the main critical section?
Are recipes immutable and built away from the machine workflow?
Does the factory isolate beverage creation from preparation orchestration?
Can multiple outlets work concurrently without corrupting shared stock?
Did you explain singleton scope as one shared instance per physical device, not global mutable state everywhere?
Can the design support low-stock alerts and refill without rewriting dispense logic?
Quiz
0/5 answered
1.Why should IngredientInventory.consume check all ingredients before deducting any of them?
2.What does the semaphore in CoffeeMachine model?
3.Why use Recipe.Builder for drink recipes?
4.Where should supported beverage names such as latte and cappuccino be mapped to recipes?
5.What is the safest way to apply Singleton in this design?
Practice Variants
Add configurable recipes
IntermediateLoad recipes from a catalog and validate that every referenced ingredient exists before publishing the catalog.
Add low-stock alerts
BeginnerCreate a threshold policy that reads inventory snapshots and reports ingredients requiring refill after each preparation.
Add request queueing
IntermediateQueue beverage requests when all outlets are busy, preserve FIFO ordering, and surface estimated wait time.
Add hardware failure recovery
AdvancedRecord preparation attempts and decide how to compensate when an ingredient was deducted but a pump or grinder failed.
Flashcards
Cheat Sheet
Entities: CoffeeMachine, IngredientInventory, Ingredient, Recipe, Recipe.Builder, Beverage, BeverageFactory, InsufficientIngredientException.
Patterns: Builder for immutable recipes, Factory Method for beverage creation, Singleton at the physical machine composition boundary.
Flow: prepare = acquire outlet permit → create beverage → consume recipe ingredients atomically → brew → release permit.
Inventory invariant: check every required ingredient before deducting any ingredient; failed requests leave stock unchanged.
Concurrency: semaphore limits active outlets; synchronized inventory methods protect shared stock.
Extend: add beverages in the factory, add ingredients in the enum, add low-stock alerts from inventory snapshots.
References
- BookHead First Design Patterns — Freeman and Robson
- BookJava Concurrency in Practice — Brian Goetz
- DocsRefactoring Guru — Builder Pattern
- DocsRefactoring Guru — Factory Method Pattern