← Back to Design & Development
Low-Level Design

Vending Machine

Complete interview-ready design — State Pattern, Chain of Responsibility, full code & edge cases

Step 1

Clarify Requirements

Always start by asking these questions to the interviewer before designing anything.

✅ Functional Requirements

  • Machine holds multiple products in slots (with quantity)
  • User selects a product, inserts coins/notes
  • Machine validates payment ≥ product price
  • Machine dispenses product & returns change
  • User can cancel transaction and get refund
  • Admin can add/refill products & collect money
  • Multiple coin denominations (₹1, ₹5, ₹10)

❌ Out of Scope (clarify these)

  • Digital payments (UPI, card) — keep it coin-based
  • Multiple vending machines / distributed system
  • User authentication or loyalty programs
  • Inventory management across locations
  • Temperature control (cold drinks vs snacks)
  • Display/UI rendering
Step 2

Core Use Cases

🟢 Happy Path

  • Select product
  • Insert sufficient coins
  • Get product + change

🟡 Cancel Flow

  • Select product
  • Insert some coins
  • Press cancel
  • Get full refund

🔴 Edge Cases

  • Product out of stock
  • Insufficient payment
  • Machine can't give change
  • No product selected
Step 3

Identify Entities

┌─────────────────────────────────────────────────────────────────────┐
│                        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()                                                │
│                                                                      │
└─────────────────────────────────────────────────────────────────────┘
Step 4

State Machine (The Heart of This Design)

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()         │   ❌    │   ✅   │    ✅    │    ❌    │
  └──────────────────┴─────────┴────────┴──────────┴──────────┘
Why State Pattern? Without it, you'd have massive if-else chains in every method checking the current state. State pattern makes each state a separate class, each handling only its valid operations. Adding a new state = adding a new class (Open-Closed Principle).
Step 5

Design Patterns Used

🔄 State Pattern

Machine behavior changes based on state — Idle, ProductSelected, HasMoney, Dispense. Each state encapsulates its own behavior.

⛓️ Chain of Responsibility

For coin change calculation — ₹10 handler → ₹5 handler → ₹1 handler. Each handler dispenses what it can, passes remainder to next.

🏭 Singleton

VendingMachine is a single instance. Only one machine object exists at runtime. Thread-safe if needed.

📦 Enum Pattern

Coin denominations as enum with values. Clean, type-safe, and prevents invalid coin types.

Step 6

Complete Code Implementation

Write these classes during the interview. Prioritize: Enums → Models → State Interface → Each State → VendingMachine → Chain of Responsibility.

Coin.java — Enum
public 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);
    }
}
⭐ State Interface — Most Important Part — This is what interviewers evaluate most closely. Each method represents a user action. Invalid actions in a state throw exceptions.
State.java — Interface
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");
    }
}
⛓️ Chain of Responsibility — For returning change in minimum coins. Each handler tries its denomination, passes remainder to the next handler.
CoinChangeHandler.java
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());
        }
    }
}
Step 7

Class Diagram (Draw This on Whiteboard)

┌─────────────────────────────────────────────────────────────────────────────┐
│                          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)     │                                                  │
│   └──────────────────────┘                                                  │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
Step 8

Edge Cases & How We Handle Them

Mentioning these proactively shows depth and impresses interviewers.

🔴 Product Out of Stock

IdleState.selectProduct() checks shelf.isAvailable() before transitioning. Throws exception if quantity is 0.

🔴 Insufficient Payment

HasMoneyState.pressDispense() compares inserted vs price. If less, stays in HasMoneyState and prompts user to insert more.

🔴 Invalid Actions in Wrong State

Each state throws RuntimeException for invalid operations. E.g., can't insertCoin() in IdleState — must select product first.

🔴 Cancel Mid-Transaction

Both ProductSelectedState and HasMoneyState support cancel(). Coins are fully refunded via CoinChangeHandler chain.

🔴 Machine Can't Return Exact Change

Advanced: Track coin inventory in the machine. Before dispensing, verify change can be made. If not, reject the transaction or ask for exact change.

🔴 Concurrent Access

In a real system, use synchronized blocks or ReentrantLock on state transitions to prevent race conditions with multiple users.

🔴 Invalid Product Code

Inventory.getShelf() throws exception for unknown codes. No silent failures — always fail loud.

🔴 Power Failure During Dispense

Discussion point: Persist state to disk before dispensing. On restart, check if product was physically dispensed. This is an advanced consideration.

Step 9

How This Design Follows SOLID

S — Single Responsibility: Each state class handles only its own behavior. Inventory manages stock. CoinChangeHandler handles change. VendingMachine orchestrates.
O — Open/Closed: Adding a new state (e.g., MaintenanceState) = new class. No modification to existing states. Adding a new coin denomination = new enum value + chain link.
L — Liskov Substitution: Any State implementation can replace another in VendingMachine. The machine doesn't know which concrete state it's using.
I — Interface Segregation: State interface has only 4 focused methods. We don't force states to implement admin operations (refill, collectMoney).
D — Dependency Inversion: VendingMachine depends on State (interface), not IdleState (concrete). Easy to test with mock states.
Step 10

Interview Q&A — What They'll Ask

Why did you choose the State pattern here?
Answer: The vending machine has clearly defined states with different valid operations in each. Without the State pattern, every method would need a giant if-else checking the current state — that's fragile and violates OCP. With the State pattern, each state is a class that encapsulates its behavior, and state transitions are clean and explicit.
What if I want to add a "Maintenance Mode" where the machine is locked?
Answer: Just create a new MaintenanceState implementing State. All 4 methods throw "Machine is under maintenance." Admin calls machine.setState(new MaintenanceState()). Zero changes to existing code — pure OCP.
Why Chain of Responsibility for change instead of a simple method?
Answer: It's extensible — adding a new coin denomination (₹2) means adding one handler and linking it in the chain. It also follows SRP — each handler only knows about its own denomination. However, for an interview, I'd mention that a simple greedy loop would also work for this specific problem. The pattern shines when you need dynamic chains or conditional processing.
How would you handle thread safety?
Answer: Wrap state transitions in a 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.
What if the machine doesn't have enough coins to return change?
Answer: Add a 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.
Can you make this more testable?
Answer: Already fairly testable since we depend on the State interface — we can mock it in unit tests. For even better testability: inject Inventory and State via constructor (Dependency Injection), use an interface for CoinChangeHandler to mock change calculation, and use events/callbacks instead of System.out.println for verifying output.
What's the time complexity of the change algorithm?
Answer: The chain iterates through denominations (constant, ~4 coin types), and for each does O(1) division and modulo. Total: O(D) where D is the number of denominations. This greedy approach works because our coin system is canonical (each coin divides the next). For arbitrary denominations, you'd need dynamic programming.
How would you extend this for digital payments?
Answer: Create a 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.
Quick Reference

Interview Cheat Sheet Summary

┌──────────────────────────────────────────────────────┐
│              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                              │
│                                                       │
└──────────────────────────────────────────────────────┘