Complete interview-ready design — State Pattern, Chain of Responsibility, full code & edge cases
Always start by asking these questions to the interviewer before designing anything.
┌─────────────────────────────────────────────────────────────────────┐ │ ENTITIES MAP │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ VendingMachine Product Coin (Enum) │ │ ├─ currentState ├─ name ├─ PENNY (1) │ │ ├─ inventory ├─ price ├─ NICKEL (5) │ │ ├─ selectedProduct └─ code ├─ DIME (10) │ │ ├─ totalInserted └─ QUARTER (25) │ │ └─ coinInventory ItemShelf │ │ ├─ product State (Interface) │ │ Inventory ├─ quantity ├─ IdleState │ │ ├─ shelves[] └─ shelfCode ├─ HasMoneyState │ │ ├─ addProduct() ├─ SelectionState │ │ ├─ getProduct() └─ DispenseState │ │ └─ updateQuantity() │ │ │ └─────────────────────────────────────────────────────────────────────┘
The vending machine has 4 states. Every user action transitions the machine between states. This is the State Design Pattern — the most critical pattern for this problem.
┌──────────────────────────────────────┐
│ STATE TRANSITIONS │
└──────────────────────────────────────┘
┌───────────┐ selectProduct() ┌────────────────┐
│ │ ──────────────────► │ │
│ IDLE │ │ PRODUCT │
│ STATE │ ◄────────────────── │ SELECTED │
│ │ cancel() │ STATE │
└───────────┘ └───────┬────────┘
▲ │
│ insertCoin()
│ │
│ ▼
┌────┴──────┐ change returned ┌────────────────┐
│ │ ◄────────────────── │ │
│ DISPENSE │ │ HAS MONEY │
│ STATE │ enough money? │ STATE │
│ │ ◄────────────────── │ │
└───────────┘ dispenseProduct() └────────────────┘
│ │
│ product dispensed │
└──────────► back to IDLE ◄──────────┘
cancel() → refund
Each state only allows VALID operations:
┌──────────────────┬─────────┬────────┬──────────┬──────────┐
│ Action │ Idle │ Select │ HasMoney │ Dispense │
├──────────────────┼─────────┼────────┼──────────┼──────────┤
│ selectProduct() │ ✅ │ ❌ │ ❌ │ ❌ │
│ insertCoin() │ ❌ │ ✅ │ ✅ │ ❌ │
│ pressButton() │ ❌ │ ❌ │ ✅ │ ❌ │
│ dispenseProduct()│ ❌ │ ❌ │ ❌ │ ✅ │
│ cancel() │ ❌ │ ✅ │ ✅ │ ❌ │
└──────────────────┴─────────┴────────┴──────────┴──────────┘
Machine behavior changes based on state — Idle, ProductSelected, HasMoney, Dispense. Each state encapsulates its own behavior.
For coin change calculation — ₹10 handler → ₹5 handler → ₹1 handler. Each handler dispenses what it can, passes remainder to next.
VendingMachine is a single instance. Only one machine object exists at runtime. Thread-safe if needed.
Coin denominations as enum with values. Clean, type-safe, and prevents invalid coin types.
Write these classes during the interview. Prioritize: Enums → Models → State Interface → Each State → VendingMachine → Chain of Responsibility.
Coin.java — Enumpublic enum Coin { PENNY(1), NICKEL(5), DIME(10), QUARTER(25); public final int value; Coin(int value) { this.value = value; } }Product.java
public class Product { private String name; private int price; private String code; // e.g., "A1", "B3" public Product(String name, int price, String code) { this.name = name; this.price = price; this.code = code; } // getters public String getName() { return name; } public int getPrice() { return price; } public String getCode() { return code; } }ItemShelf.java
public class ItemShelf { private Product product; private int quantity; private String shelfCode; public ItemShelf(Product product, int quantity, String code) { this.product = product; this.quantity = quantity; this.shelfCode = code; } public boolean isAvailable() { return quantity > 0; } public void reduceQuantity() { quantity--; } // getters & setters public Product getProduct() { return product; } public int getQuantity() { return quantity; } public String getShelfCode() { return shelfCode; } public void setQuantity(int q) { this.quantity = q; } }Inventory.java
public class Inventory { private Map<String, ItemShelf> shelves; // code → shelf public Inventory() { this.shelves = new HashMap<>(); } public void addProduct(Product product, int qty, String code) { shelves.put(code, new ItemShelf(product, qty, code)); } public ItemShelf getShelf(String code) { if (!shelves.containsKey(code)) throw new RuntimeException("Invalid product code: " + code); return shelves.get(code); } public Map<String, ItemShelf> getAllShelves() { return Collections.unmodifiableMap(shelves); } }
public interface State { void selectProduct(VendingMachine machine, String code); void insertCoin(VendingMachine machine, Coin coin); void pressDispenseButton(VendingMachine machine); void cancel(VendingMachine machine); }IdleState.java
public class IdleState implements State { @Override public void selectProduct(VendingMachine machine, String code) { ItemShelf shelf = machine.getInventory().getShelf(code); if (!shelf.isAvailable()) { throw new RuntimeException("Product out of stock: " + code); } System.out.println("Selected: " + shelf.getProduct().getName() + " | Price: " + shelf.getProduct().getPrice()); machine.setSelectedProduct(shelf.getProduct()); machine.setState(new ProductSelectedState()); } @Override public void insertCoin(VendingMachine m, Coin coin) { throw new RuntimeException("Please select a product first"); } @Override public void pressDispenseButton(VendingMachine m) { throw new RuntimeException("Please select a product first"); } @Override public void cancel(VendingMachine m) { System.out.println("Nothing to cancel"); } }ProductSelectedState.java
public class ProductSelectedState implements State { @Override public void selectProduct(VendingMachine m, String code) { throw new RuntimeException("Product already selected. Insert coins."); } @Override public void insertCoin(VendingMachine machine, Coin coin) { machine.addCoin(coin); System.out.println("Inserted: " + coin.value + " | Total: " + machine.getTotalInserted()); machine.setState(new HasMoneyState()); } @Override public void pressDispenseButton(VendingMachine m) { throw new RuntimeException("Please insert coins first"); } @Override public void cancel(VendingMachine machine) { System.out.println("Selection cancelled."); machine.setSelectedProduct(null); machine.setState(new IdleState()); } }HasMoneyState.java
public class HasMoneyState implements State { @Override public void selectProduct(VendingMachine m, String code) { throw new RuntimeException("Product already selected"); } @Override public void insertCoin(VendingMachine machine, Coin coin) { machine.addCoin(coin); System.out.println("Inserted: " + coin.value + " | Total: " + machine.getTotalInserted()); // Stay in HasMoneyState — user might add more coins } @Override public void pressDispenseButton(VendingMachine machine) { int price = machine.getSelectedProduct().getPrice(); int inserted = machine.getTotalInserted(); if (inserted < price) { System.out.println("Insufficient! Need " + (price - inserted) + " more."); return; // stay in HasMoneyState } // Enough money — transition to Dispense machine.setState(new DispenseState()); machine.getState().pressDispenseButton(machine); } @Override public void cancel(VendingMachine machine) { System.out.println("Cancelled. Refunding: " + machine.getTotalInserted()); machine.refundFullAmount(); machine.setSelectedProduct(null); machine.setState(new IdleState()); } }DispenseState.java
public class DispenseState implements State { @Override public void selectProduct(VendingMachine m, String c) { throw new RuntimeException("Dispensing in progress..."); } @Override public void insertCoin(VendingMachine m, Coin c) { throw new RuntimeException("Dispensing in progress..."); } @Override public void pressDispenseButton(VendingMachine machine) { Product product = machine.getSelectedProduct(); int price = product.getPrice(); int inserted = machine.getTotalInserted(); int change = inserted - price; // 1. Dispense the product System.out.println("🎉 Dispensing: " + product.getName()); // 2. Reduce inventory String code = product.getCode(); machine.getInventory().getShelf(code).reduceQuantity(); // 3. Return change using Chain of Responsibility if (change > 0) { System.out.println("Returning change: " + change); CoinChangeHandler.returnChange(change); } // 4. Reset machine state machine.resetTransaction(); machine.setState(new IdleState()); } @Override public void cancel(VendingMachine m) { throw new RuntimeException("Cannot cancel during dispensing"); } }
public abstract class CoinChangeHandler { protected CoinChangeHandler nextHandler; public void setNext(CoinChangeHandler next) { this.nextHandler = next; } public abstract void dispense(int amount); // Factory method to build the chain public static void returnChange(int amount) { CoinChangeHandler quarter = new CoinDispenser(25, "QUARTER"); CoinChangeHandler dime = new CoinDispenser(10, "DIME"); CoinChangeHandler nickel = new CoinDispenser(5, "NICKEL"); CoinChangeHandler penny = new CoinDispenser(1, "PENNY"); quarter.setNext(dime); dime.setNext(nickel); nickel.setNext(penny); quarter.dispense(amount); } } public class CoinDispenser extends CoinChangeHandler { private int coinValue; private String coinName; public CoinDispenser(int value, String name) { this.coinValue = value; this.coinName = name; } @Override public void dispense(int amount) { if (amount >= coinValue) { int count = amount / coinValue; int remainder = amount % coinValue; System.out.println(" Returning " + count + " × " + coinName); if (remainder > 0 && nextHandler != null) { nextHandler.dispense(remainder); } } else if (nextHandler != null) { nextHandler.dispense(amount); } } }VendingMachine.java — Main Class
public class VendingMachine { private State currentState; private Inventory inventory; private Product selectedProduct; private List<Coin> insertedCoins; public VendingMachine() { this.currentState = new IdleState(); this.inventory = new Inventory(); this.insertedCoins = new ArrayList<>(); } // ── User Actions (delegated to current State) ── public void selectProduct(String code) { currentState.selectProduct(this, code); } public void insertCoin(Coin coin) { currentState.insertCoin(this, coin); } public void pressDispenseButton() { currentState.pressDispenseButton(this); } public void cancel() { currentState.cancel(this); } // ── Internal helpers ── public void addCoin(Coin coin) { insertedCoins.add(coin); } public int getTotalInserted() { return insertedCoins.stream() .mapToInt(c -> c.value) .sum(); } public void refundFullAmount() { int total = getTotalInserted(); if (total > 0) { CoinChangeHandler.returnChange(total); } insertedCoins.clear(); } public void resetTransaction() { selectedProduct = null; insertedCoins.clear(); } // ── Getters & Setters ── public State getState() { return currentState; } public void setState(State s) { this.currentState = s; } public Inventory getInventory() { return inventory; } public Product getSelectedProduct() { return selectedProduct; } public void setSelectedProduct(Product p) { this.selectedProduct = p; } }Main.java — Demo All Use Cases
public class Main { public static void main(String[] args) { VendingMachine vm = new VendingMachine(); // Admin: Stock the machine vm.getInventory().addProduct( new Product("Coke", 25, "A1"), 5, "A1"); vm.getInventory().addProduct( new Product("Chips", 15, "B2"), 3, "B2"); vm.getInventory().addProduct( new Product("Candy", 10, "C3"), 0, "C3"); // out of stock // ───────── USE CASE 1: Happy Path ───────── System.out.println("═══ USE CASE 1: Happy Path ═══"); vm.selectProduct("A1"); // Idle → ProductSelected vm.insertCoin(Coin.DIME); // ProductSelected → HasMoney vm.insertCoin(Coin.DIME); // stay in HasMoney vm.insertCoin(Coin.DIME); // total = 30, price = 25 vm.pressDispenseButton(); // HasMoney → Dispense → Idle // Output: Dispensing Coke, Returning change: 5 (1 × NICKEL) // ───────── USE CASE 2: Cancel After Selection ───────── System.out.println("\n═══ USE CASE 2: Cancel ═══"); vm.selectProduct("B2"); vm.insertCoin(Coin.QUARTER); vm.cancel(); // Refund 25, → Idle // ───────── USE CASE 3: Out of Stock ───────── System.out.println("\n═══ USE CASE 3: Out of Stock ═══"); try { vm.selectProduct("C3"); // throws RuntimeException } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } // ───────── USE CASE 4: Insufficient Payment ───────── System.out.println("\n═══ USE CASE 4: Insufficient ═══"); vm.selectProduct("A1"); vm.insertCoin(Coin.NICKEL); // only 5, need 25 vm.pressDispenseButton(); // prints "Need 20 more" vm.insertCoin(Coin.QUARTER); // now 30, enough vm.pressDispenseButton(); // dispenses + change 5 // ───────── USE CASE 5: Invalid Action ───────── System.out.println("\n═══ USE CASE 5: Invalid Action ═══"); try { vm.insertCoin(Coin.DIME); // In Idle state — error! } catch (Exception e) { System.out.println("Error: " + e.getMessage()); } } }
┌─────────────────────────────────────────────────────────────────────────────┐ │ CLASS RELATIONSHIPS │ ├─────────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌──────────────┐ │ │ │ «interface» │ │ │ │ State │ │ │ │──────────────│ │ │ │+selectProduct │ │ │ │+insertCoin │ │ │ │+pressDispense │ │ │ │+cancel │ │ │ └──────┬───────┘ │ │ ┌───────┬───┴───┬──────────┐ │ │ ▼ ▼ ▼ ▼ │ │ ┌─────────┐ ┌────────┐ ┌─────────┐ ┌──────────┐ │ │ │IdleState│ │Product │ │HasMoney │ │Dispense │ │ │ │ │ │Selected│ │State │ │State │ │ │ │ │ │State │ │ │ │ │ │ │ └─────────┘ └────────┘ └─────────┘ └──────────┘ │ │ │ │ ┌──────────────────┐ ┌────────────┐ ┌──────────────┐ │ │ │ VendingMachine │ has──►│ Inventory │has──►│ ItemShelf │ │ │ │──────────────────│ │────────────│ │──────────────│ │ │ │- currentState │ │- shelves │ │- product │ │ │ │- selectedProduct │ │+addProduct │ │- quantity │ │ │ │- insertedCoins │ │+getShelf │ │+isAvailable │ │ │ │+selectProduct() │ └────────────┘ │+reduceQty │ │ │ │+insertCoin() │ └──────┬───────┘ │ │ │+pressDispense() │ ┌────────────┐ │ has │ │ │+cancel() │ │ «enum» │ ▼ │ │ └──────────────────┘ │ Coin │ ┌──────────────┐ │ │ │────────────│ │ Product │ │ │ ┌──────────────────────┐ │ PENNY(1) │ │──────────────│ │ │ │ «abstract» │ │ NICKEL(5) │ │- name │ │ │ │ CoinChangeHandler │ │ DIME(10) │ │- price │ │ │ │──────────────────────│ │ QUARTER(25) │ │- code │ │ │ │- nextHandler │ └────────────┘ └──────────────┘ │ │ │+setNext() │ │ │ │+dispense(amount) │ │ │ └──────────┬───────────┘ │ │ ▼ │ │ ┌──────────────────────┐ │ │ │ CoinDispenser │ │ │ │──────────────────────│ │ │ │- coinValue │ │ │ │- coinName │ │ │ │+dispense(amount) │ │ │ └──────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘
Mentioning these proactively shows depth and impresses interviewers.
IdleState.selectProduct() checks shelf.isAvailable() before transitioning. Throws exception if quantity is 0.
HasMoneyState.pressDispense() compares inserted vs price. If less, stays in HasMoneyState and prompts user to insert more.
Each state throws RuntimeException for invalid operations. E.g., can't insertCoin() in IdleState — must select product first.
Both ProductSelectedState and HasMoneyState support cancel(). Coins are fully refunded via CoinChangeHandler chain.
Advanced: Track coin inventory in the machine. Before dispensing, verify change can be made. If not, reject the transaction or ask for exact change.
In a real system, use synchronized blocks or ReentrantLock on state transitions to prevent race conditions with multiple users.
Inventory.getShelf() throws exception for unknown codes. No silent failures — always fail loud.
Discussion point: Persist state to disk before dispensing. On restart, check if product was physically dispensed. This is an advanced consideration.
MaintenanceState implementing State. All 4 methods throw "Machine is under maintenance." Admin calls machine.setState(new MaintenanceState()). Zero changes to existing code — pure OCP.ReentrantLock. The critical section is: check state → perform action → transition state. This must be atomic. Alternatively, use synchronized on the VendingMachine methods. In production, I'd prefer lock-free designs or actor-based models for high throughput.CoinInventory that tracks available coins in the machine. Before transitioning to DispenseState, verify that change can be assembled from available coins. If not, show "EXACT CHANGE ONLY" and reject over-payments, or refund and cancel the transaction.PaymentStrategy interface with implementations: CoinPayment, CardPayment, UPIPayment. The State classes delegate to the active payment strategy. This decouples payment logic from state management — Strategy + State patterns together.┌──────────────────────────────────────────────────────┐ │ VENDING MACHINE LLD SUMMARY │ ├──────────────────────────────────────────────────────┤ │ │ │ Entities: VendingMachine, Inventory, ItemShelf, │ │ Product, Coin (enum), State (interface) │ │ │ │ States: Idle → ProductSelected → HasMoney │ │ → Dispense → back to Idle │ │ │ │ Patterns: State (core), Chain of Responsibility │ │ (change), Singleton (optional), Enum │ │ │ │ Key Classes: 4 State classes + VendingMachine │ │ + Inventory + CoinChangeHandler chain │ │ │ │ Edge Cases: Out of stock, insufficient money, │ │ cancel/refund, invalid state actions, │ │ exact change, thread safety │ │ │ │ SOLID: ✓ All 5 principles demonstrated │ │ │ │ Time in Interview: ~30-35 minutes │ │ Requirements: 3 min │ │ Entities: 3 min │ │ State Diagram: 5 min │ │ Core Code: 15 min │ │ Discussion: 5 min │ │ │ └──────────────────────────────────────────────────────┘