LLD: Design Coffee Vending Machine
Coffee Vending Machine, written with OOD principles, extensibility, and concurrency .
Each beverage requires certain ingredients in fixed quantities
Machine should:
Check ingredient availability
Deduct ingredients atomically
Prepare the drink
Support multiple outlets (parallel preparation)
Show errors:
Insufficient ingredients
Invalid beverage
Non-Functional Requirements
Thread-safe (multiple users / outlets)
Extensible (add new drinks easily)
Low latency
Consistent ingredient state
2. Core Design Concepts
| Concept | Responsibility |
| --------------------- | ------------------------------ |
| `CoffeeMachine` | Orchestrates entire flow |
| `Beverage` | Defines ingredients & quantity |
| `IngredientInventory` | Tracks ingredient levels |
| `BeverageFactory` | Creates beverage objects |
| `Outlet` | Prepares beverage concurrently |
3. Class Diagram (Logical View)
CoffeeMachine
├── IngredientInventory
├── List<Outlet>
└── BeverageFactory
Beverage (interface)
├── Espresso
├── Cappuccino
└── Latte
IngredientInventory
├── Map<String, Integer>
├── checkAvailability()
└── consumeIngredients()
Outlet (Runnable)
└── prepareBeverage()
4. Class Design (Java)
4.1 Beverage Interface
public interface Beverage {
String getName();
Map<String, Integer> getIngredients();
}
4.2 Concrete Beverages
public class Espresso implements Beverage {
@Override
public String getName() {
return "Espresso";
}
@Override
public Map<String, Integer> getIngredients() {
Map<String, Integer> ingredients = new HashMap<>();
ingredients.put("Coffee", 50);
ingredients.put("Water", 100);
return ingredients;
}
}
public class Cappuccino implements Beverage {
@Override
public String getName() {
return "Cappuccino";
}
@Override
public Map<String, Integer> getIngredients() {
Map<String, Integer> ingredients = new HashMap<>();
ingredients.put("Coffee", 50);
ingredients.put("Milk", 100);
ingredients.put("Water", 50);
return ingredients;
}
}
➡️ Open–Closed Principle: New drinks can be added without touching existing logic.
4.3 Beverage Factory
public class BeverageFactory {
public static Beverage createBeverage(String type) {
switch (type.toLowerCase()) {
case "espresso":
return new Espresso();
case "cappuccino":
return new Cappuccino();
default:
throw new IllegalArgumentException("Invalid beverage");
}
}
}
5. Ingredient Inventory (Thread-Safe)
public class IngredientInventory {
private final Map<String, Integer> inventory = new HashMap<>();
public IngredientInventory(Map<String, Integer> initialStock) {
inventory.putAll(initialStock);
}
public synchronized boolean hasIngredients(Map<String, Integer> required) {
for (Map.Entry<String, Integer> entry : required.entrySet()) {
if (inventory.getOrDefault(entry.getKey(), 0) < entry.getValue()) {
return false;
}
}
return true;
}
public synchronized void consumeIngredients(Map<String, Integer> required) {
for (Map.Entry<String, Integer> entry : required.entrySet()) {
inventory.put(
entry.getKey(),
inventory.get(entry.getKey()) - entry.getValue()
);
}
}
}
✔ Atomic check + consume
✔ Prevents race conditions
6. Outlet (Parallel Processing)
public class Outlet implements Runnable {
private final IngredientInventory inventory;
private final Beverage beverage;
public Outlet(IngredientInventory inventory, Beverage beverage) {
this.inventory = inventory;
this.beverage = beverage;
}
@Override
public void run() {
synchronized (inventory) {
if (!inventory.hasIngredients(beverage.getIngredients())) {
System.out.println(
beverage.getName() + " cannot be prepared due to insufficient ingredients"
);
return;
}
inventory.consumeIngredients(beverage.getIngredients());
}
System.out.println("Preparing " + beverage.getName());
System.out.println(beverage.getName() + " is ready");
}
}
7. Coffee Machine (Main Orchestrator)
public class CoffeeMachine {
private final IngredientInventory inventory;
private final ExecutorService executor;
public CoffeeMachine(int outlets, IngredientInventory inventory) {
this.inventory = inventory;
this.executor = Executors.newFixedThreadPool(outlets);
}
public void orderBeverage(String beverageType) {
Beverage beverage = BeverageFactory.createBeverage(beverageType);
executor.submit(new Outlet(inventory, beverage));
}
public void shutdown() {
executor.shutdown();
}
}
8. Main Driver
public class Main {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
stock.put("Coffee", 500);
stock.put("Milk", 500);
stock.put("Water", 1000);
IngredientInventory inventory = new IngredientInventory(stock);
CoffeeMachine machine = new CoffeeMachine(3, inventory);
machine.orderBeverage("Espresso");
machine.orderBeverage("Cappuccino");
machine.orderBeverage("Espresso");
machine.shutdown();
}
}
9. Design Patterns Used
| Pattern | Usage |
| -------------------- | ------------------------ |
| Factory | Beverage creation |
| Strategy | Beverage recipe |
| Singleton (optional) | Inventory |
| Executor Service | Multi-outlet concurrency |
10. Interview Follow-Ups You Can Answer
How to refill ingredients?
→ AddrefillIngredient()inIngredientInventoryHow to add pricing?
→ AddgetPrice()inBeverageHow to support failures mid-brew?
→ Rollback inventory (transactional logic)How to scale for 1000 machines?
→ Central inventory service + event sourcing

