Design a Vending Machine

intermediate 2-4 YOE lld vending-machine design-question state-pattern

The vending machine is the classic State pattern interview question. We already learned the State pattern earlier — now we’re applying it to a full design problem with real inventory, money handling, and change calculation. Interviewers love this because it tests whether we actually know when and how to use design patterns in practice.

The whole point: the machine behaves completely differently depending on what state it’s in. Insert money when idle? Cool, accepted. Insert money when already dispensing? Nope, wait. Same action, different behavior based on state.

Requirements

Functional:

  • Machine holds multiple products, each with a price and quantity
  • User inserts coins/money (we track the running total)
  • User selects a product
  • Machine dispenses product if enough money and in stock
  • Machine returns change if overpaid
  • Machine handles edge cases: out of stock, insufficient money, no change available

State Transitions:

  • Idle -> (insert money) -> HasMoney
  • HasMoney -> (select product, enough money) -> Dispensing
  • HasMoney -> (cancel) -> Idle (refund money)
  • Dispensing -> (product dispensed) -> Idle

Key Classes & Relationships

Vending Machine State Diagram
IdleState
accepts money
rejects select/dispense
--insert $-->
HasMoneyState
accepts more money
allows product selection
--select-->
DispensingState
dispenses product
returns change
goes back to Idle
cancel at any point -> refund money -> Idle
Product
name, price, code
Inventory
products + quantities
VendingMachine
state, balance, inventory

Core Implementation

from abc import ABC, abstractmethod

# ---- Product & Inventory ----
class Product:
    def __init__(self, code: str, name: str, price: float):
        self.code = code
        self.name = name
        self.price = price

    def __repr__(self):
        return f"{self.name} (${self.price})"

class Inventory:
    def __init__(self):
        self.products = {}   # code -> Product
        self.quantities = {} # code -> int

    def add_product(self, product: Product, quantity: int):
        self.products[product.code] = product
        self.quantities[product.code] = self.quantities.get(product.code, 0) + quantity

    def get_product(self, code: str) -> Product:
        return self.products.get(code)

    def is_available(self, code: str) -> bool:
        return self.quantities.get(code, 0) > 0

    def dispense(self, code: str):
        if self.is_available(code):
            self.quantities[code] -= 1

# ---- State Interface ----
class State(ABC):
    @abstractmethod
    def insert_money(self, machine, amount: float) -> None:
        pass

    @abstractmethod
    def select_product(self, machine, code: str) -> None:
        pass

    @abstractmethod
    def dispense(self, machine) -> None:
        pass

    @abstractmethod
    def cancel(self, machine) -> None:
        pass

# ---- Concrete States ----
class IdleState(State):
    def insert_money(self, machine, amount):
        machine.balance += amount
        print(f"Inserted ${amount}. Balance: ${machine.balance}")
        machine.set_state(HasMoneyState())

    def select_product(self, machine, code):
        print("Insert money first!")

    def dispense(self, machine):
        print("Insert money and select a product first!")

    def cancel(self, machine):
        print("Nothing to cancel.")

class HasMoneyState(State):
    def insert_money(self, machine, amount):
        machine.balance += amount
        print(f"Inserted ${amount}. Balance: ${machine.balance}")

    def select_product(self, machine, code):
        product = machine.inventory.get_product(code)

        if not product:
            print(f"Invalid code: {code}")
            return

        if not machine.inventory.is_available(code):
            print(f"{product.name} is out of stock!")
            return

        if machine.balance < product.price:
            print(f"Not enough money! {product.name} costs ${product.price}, "
                  f"balance is ${machine.balance}. Insert ${product.price - machine.balance} more.")
            return

        machine.selected_product = product
        print(f"Selected: {product.name}. Dispensing...")
        machine.set_state(DispensingState())
        machine.dispense()  # auto-trigger dispense

    def dispense(self, machine):
        print("Select a product first!")

    def cancel(self, machine):
        print(f"Cancelled. Refunding ${machine.balance}")
        machine.balance = 0
        machine.set_state(IdleState())

class DispensingState(State):
    def insert_money(self, machine, amount):
        print("Please wait, dispensing in progress.")

    def select_product(self, machine, code):
        print("Already dispensing, please wait.")

    def dispense(self, machine):
        product = machine.selected_product
        machine.inventory.dispense(product.code)

        change = machine.balance - product.price
        print(f"Dispensed: {product.name}")
        if change > 0:
            print(f"Change returned: ${change}")

        # Reset
        machine.balance = 0
        machine.selected_product = None
        machine.set_state(IdleState())

    def cancel(self, machine):
        print("Cannot cancel during dispensing.")

# ---- Vending Machine ----
class VendingMachine:
    def __init__(self):
        self.inventory = Inventory()
        self.balance = 0.0
        self.selected_product = None
        self._state = IdleState()

    def set_state(self, state: State):
        self._state = state

    def insert_money(self, amount: float):
        self._state.insert_money(self, amount)

    def select_product(self, code: str):
        self._state.select_product(self, code)

    def dispense(self):
        self._state.dispense(self)

    def cancel(self):
        self._state.cancel(self)

    def show_products(self):
        print("\n--- Available Products ---")
        for code, product in self.inventory.products.items():
            qty = self.inventory.quantities[code]
            status = f"({qty} left)" if qty > 0 else "(OUT OF STOCK)"
            print(f"  [{code}] {product.name} - ${product.price} {status}")
        print()

# ---- Usage ----
machine = VendingMachine()

# Stock the machine
machine.inventory.add_product(Product("A1", "Coca Cola", 1.50), 5)
machine.inventory.add_product(Product("A2", "Pepsi", 1.50), 3)
machine.inventory.add_product(Product("B1", "Lays Chips", 2.00), 2)
machine.inventory.add_product(Product("B2", "Snickers", 1.75), 0)  # out of stock!

machine.show_products()

# Happy path
machine.insert_money(2.00)       # Inserted $2.0. Balance: $2.0
machine.select_product("A1")     # Selected: Coca Cola. Dispensing...
                                  # Dispensed: Coca Cola
                                  # Change returned: $0.5

# Not enough money
machine.insert_money(1.00)       # Inserted $1.0. Balance: $1.0
machine.select_product("B1")     # Not enough money! Lays costs $2.0...

# Add more money
machine.insert_money(1.00)       # Inserted $1.0. Balance: $2.0
machine.select_product("B1")     # Selected: Lays Chips. Dispensing...

# Out of stock
machine.insert_money(2.00)
machine.select_product("B2")     # Snickers is out of stock!
machine.cancel()                 # Cancelled. Refunding $2.0
// ---- Product & Inventory ----
class Product {
  constructor(code, name, price) {
    this.code = code;
    this.name = name;
    this.price = price;
  }
}

class Inventory {
  constructor() {
    this.products = new Map();
    this.quantities = new Map();
  }
  addProduct(product, qty) {
    this.products.set(product.code, product);
    this.quantities.set(product.code, (this.quantities.get(product.code) || 0) + qty);
  }
  getProduct(code) { return this.products.get(code) || null; }
  isAvailable(code) { return (this.quantities.get(code) || 0) > 0; }
  dispense(code) {
    if (this.isAvailable(code)) this.quantities.set(code, this.quantities.get(code) - 1);
  }
}

// ---- States ----
class IdleState {
  insertMoney(machine, amount) {
    machine.balance += amount;
    console.log(`Inserted $${amount}. Balance: $${machine.balance}`);
    machine.setState(new HasMoneyState());
  }
  selectProduct(machine, code) { console.log("Insert money first!"); }
  dispense(machine) { console.log("Insert money and select a product first!"); }
  cancel(machine) { console.log("Nothing to cancel."); }
}

class HasMoneyState {
  insertMoney(machine, amount) {
    machine.balance += amount;
    console.log(`Inserted $${amount}. Balance: $${machine.balance}`);
  }

  selectProduct(machine, code) {
    const product = machine.inventory.getProduct(code);
    if (!product) { console.log(`Invalid code: ${code}`); return; }
    if (!machine.inventory.isAvailable(code)) {
      console.log(`${product.name} is out of stock!`); return;
    }
    if (machine.balance < product.price) {
      console.log(`Not enough! ${product.name} costs $${product.price}, ` +
        `balance is $${machine.balance}`);
      return;
    }
    machine.selectedProduct = product;
    console.log(`Selected: ${product.name}. Dispensing...`);
    machine.setState(new DispensingState());
    machine.dispense();
  }

  dispense(machine) { console.log("Select a product first!"); }
  cancel(machine) {
    console.log(`Cancelled. Refunding $${machine.balance}`);
    machine.balance = 0;
    machine.setState(new IdleState());
  }
}

class DispensingState {
  insertMoney(machine, amount) { console.log("Please wait, dispensing."); }
  selectProduct(machine, code) { console.log("Already dispensing, wait."); }
  dispense(machine) {
    const product = machine.selectedProduct;
    machine.inventory.dispense(product.code);
    const change = machine.balance - product.price;
    console.log(`Dispensed: ${product.name}`);
    if (change > 0) console.log(`Change: $${change}`);
    machine.balance = 0;
    machine.selectedProduct = null;
    machine.setState(new IdleState());
  }
  cancel(machine) { console.log("Cannot cancel during dispensing."); }
}

// ---- Vending Machine ----
class VendingMachine {
  constructor() {
    this.inventory = new Inventory();
    this.balance = 0;
    this.selectedProduct = null;
    this.state = new IdleState();
  }
  setState(state) { this.state = state; }
  insertMoney(amount) { this.state.insertMoney(this, amount); }
  selectProduct(code) { this.state.selectProduct(this, code); }
  dispense() { this.state.dispense(this); }
  cancel() { this.state.cancel(this); }
}

// Usage
const machine = new VendingMachine();
machine.inventory.addProduct(new Product("A1", "Coca Cola", 1.50), 5);
machine.inventory.addProduct(new Product("B1", "Lays Chips", 2.00), 2);

machine.insertMoney(2.00);     // Inserted $2. Balance: $2
machine.selectProduct("A1");   // Selected: Coca Cola -> Dispensed + $0.5 change
import java.util.*;

class Product {
    String code, name;
    double price;
    Product(String code, String name, double price) {
        this.code = code; this.name = name; this.price = price;
    }
}

class Inventory {
    Map<String, Product> products = new HashMap<>();
    Map<String, Integer> quantities = new HashMap<>();

    void addProduct(Product p, int qty) {
        products.put(p.code, p);
        quantities.merge(p.code, qty, Integer::sum);
    }
    Product getProduct(String code) { return products.get(code); }
    boolean isAvailable(String code) { return quantities.getOrDefault(code, 0) > 0; }
    void dispense(String code) { quantities.computeIfPresent(code, (k, v) -> v - 1); }
}

// ---- State Interface ----
interface State {
    void insertMoney(VendingMachine m, double amount);
    void selectProduct(VendingMachine m, String code);
    void dispense(VendingMachine m);
    void cancel(VendingMachine m);
}

class IdleState implements State {
    public void insertMoney(VendingMachine m, double amount) {
        m.balance += amount;
        System.out.printf("Inserted $%.2f. Balance: $%.2f%n", amount, m.balance);
        m.setState(new HasMoneyState());
    }
    public void selectProduct(VendingMachine m, String code) {
        System.out.println("Insert money first!");
    }
    public void dispense(VendingMachine m) {
        System.out.println("Insert money and select a product first!");
    }
    public void cancel(VendingMachine m) {
        System.out.println("Nothing to cancel.");
    }
}

class HasMoneyState implements State {
    public void insertMoney(VendingMachine m, double amount) {
        m.balance += amount;
        System.out.printf("Inserted $%.2f. Balance: $%.2f%n", amount, m.balance);
    }

    public void selectProduct(VendingMachine m, String code) {
        Product product = m.inventory.getProduct(code);
        if (product == null) { System.out.println("Invalid code!"); return; }
        if (!m.inventory.isAvailable(code)) {
            System.out.println(product.name + " is out of stock!"); return;
        }
        if (m.balance < product.price) {
            System.out.printf("Not enough! %s costs $%.2f, balance $%.2f%n",
                product.name, product.price, m.balance);
            return;
        }
        m.selectedProduct = product;
        System.out.println("Selected: " + product.name + ". Dispensing...");
        m.setState(new DispensingState());
        m.dispense();
    }

    public void dispense(VendingMachine m) { System.out.println("Select a product first!"); }
    public void cancel(VendingMachine m) {
        System.out.printf("Cancelled. Refunding $%.2f%n", m.balance);
        m.balance = 0;
        m.setState(new IdleState());
    }
}

class DispensingState implements State {
    public void insertMoney(VendingMachine m, double amount) {
        System.out.println("Please wait, dispensing.");
    }
    public void selectProduct(VendingMachine m, String code) {
        System.out.println("Already dispensing, wait.");
    }
    public void dispense(VendingMachine m) {
        Product p = m.selectedProduct;
        m.inventory.dispense(p.code);
        double change = m.balance - p.price;
        System.out.println("Dispensed: " + p.name);
        if (change > 0) System.out.printf("Change: $%.2f%n", change);
        m.balance = 0;
        m.selectedProduct = null;
        m.setState(new IdleState());
    }
    public void cancel(VendingMachine m) {
        System.out.println("Cannot cancel during dispensing.");
    }
}

class VendingMachine {
    Inventory inventory = new Inventory();
    double balance = 0;
    Product selectedProduct = null;
    private State state = new IdleState();

    void setState(State s) { this.state = s; }
    void insertMoney(double amount) { state.insertMoney(this, amount); }
    void selectProduct(String code) { state.selectProduct(this, code); }
    void dispense() { state.dispense(this); }
    void cancel() { state.cancel(this); }
}

Why State Pattern Here?

Look at what each state does differently for the same actions:

ActionIdleStateHasMoneyStateDispensingState
insert_moneyAccept, go to HasMoneyAccept, stayReject
select_productRejectValidate & go to DispensingReject
dispenseRejectRejectDispense, go to Idle
cancelNo-opRefund, go to IdleReject

Without the State pattern, every method would have a big if-else checking which state we’re in. With it, each state class handles its own behavior. Adding a new state (like MaintenanceState) means adding one class — zero changes to existing code.

Design Patterns Used

State — The star of the show. Each state (Idle, HasMoney, Dispensing) is its own class. The VendingMachine delegates all actions to the current state. States transition themselves.

Composition — VendingMachine composes Inventory and State. Inventory composes Products. No deep inheritance trees.

Separation of concerns — Product knows about itself. Inventory manages stock. States manage behavior. VendingMachine is just the glue.

Extensions

  • Card payment — Add a PaymentMethod interface with CashPayment and CardPayment implementations. The HasMoneyState becomes more of a “HasPayment” state.
  • Admin refill — Add an AdminService class with refill(code, quantity) and collectMoney() methods. Only accessible with an admin key/PIN.
  • Multiple product selection — Let users add multiple items to a “cart” before dispensing. The HasMoneyState tracks a list of selected products instead of one.
  • Coin denomination handling — Instead of tracking a simple balance, track individual coins inserted. Makes change calculation more realistic (can we even make change with the coins we have?).
  • Display and notifications — Add a Display class that shows current state, balance, and product info. Observer pattern to update display when state changes.

In simple language, the vending machine is the perfect warm-up for State pattern in interviews. The states are obvious, the transitions are clear, and the code practically writes itself once we identify the states. When the interviewer says “design a vending machine,” they’re really asking “show me you understand the State pattern.”