Hirestack
Hirestack
Chapter 12

Problem 8: Design a Vending Machine

📖 3 min read · 6 sections · May 2026
Asked at: Amazon Microsoft

The setup

"Design a vending machine. User inserts coins/notes, selects an item, receives the item and change. Machine has inventory of multiple items with prices."

The vending machine is the canonical State pattern problem. It has explicit operational states (idle, has money, item selected, dispensing) with allowed transitions and behavior that varies per state.

Clarifying questions

QuestionStory answer
Payment types?Coins (₹1, ₹2, ₹5, ₹10) and notes (₹10, ₹20, ₹50, ₹100). No card for v1.
Change-making?Yes — machine must dispense correct change from its coin/note inventory.
What if insufficient change available?Reject the transaction; refund inserted money.
Cancel mid-transaction?Yes — user can cancel and get refund.
Refilling inventory?Admin operation; out of scope for normal use cases.

The State machine

States:
  IDLE              — waiting for money or selection
  HAS_MONEY         — money inserted; waiting for item selection
  ITEM_SELECTED     — item chosen; verifying funds, preparing to dispense
  DISPENSING        — item physically released
  RETURNING_CHANGE  — releasing change
  
Transitions:
  IDLE + insert_money → HAS_MONEY (or stays IDLE if invalid)
  HAS_MONEY + insert_more_money → HAS_MONEY (accumulate)
  HAS_MONEY + select_item → ITEM_SELECTED (if balance >= price)
  HAS_MONEY + cancel → IDLE (refund)
  ITEM_SELECTED + confirm → DISPENSING
  DISPENSING + done → RETURNING_CHANGE (if change > 0) or IDLE
  RETURNING_CHANGE + done → IDLE
interface MachineState {
  void insertMoney(VendingMachine machine, Money money);
  void selectItem(VendingMachine machine, String itemId);
  void cancel(VendingMachine machine);
  // Some states may make some methods no-ops or errors
}

class IdleState implements MachineState {
  public void insertMoney(VendingMachine machine, Money money) {
    machine.addInsertedAmount(money.value);
    machine.transitionTo(new HasMoneyState());
  }
  public void selectItem(VendingMachine machine, String itemId) {
    // No money inserted; reject
    machine.displayMessage("Please insert money first");
  }
  public void cancel(VendingMachine machine) {}  // noop
}

class HasMoneyState implements MachineState {
  public void insertMoney(VendingMachine machine, Money money) {
    machine.addInsertedAmount(money.value);  // accumulate
  }
  public void selectItem(VendingMachine machine, String itemId) {
    Item item = machine.getInventory().getItem(itemId);
    if (item == null || item.quantity == 0) {
      machine.displayMessage("Item unavailable");
      return;
    }
    if (machine.insertedAmount.compareTo(item.price) < 0) {
      machine.displayMessage("Insufficient funds; need " 
          + item.price.subtract(machine.insertedAmount) + " more");
      return;
    }
    // Check change availability
    BigDecimal changeNeeded = machine.insertedAmount.subtract(item.price);
    if (!machine.canMakeChange(changeNeeded)) {
      machine.displayMessage("Exact change required");
      machine.refund();
      machine.transitionTo(new IdleState());
      return;
    }
    machine.setSelectedItem(item);
    machine.transitionTo(new ItemSelectedState());
  }
  public void cancel(VendingMachine machine) {
    machine.refund();
    machine.transitionTo(new IdleState());
  }
}

class ItemSelectedState implements MachineState {
  public void selectItem(VendingMachine machine, String itemId) {
    // Same as confirm — proceed to dispense
    machine.dispenseSelectedItem();
    machine.transitionTo(new DispensingState());
  }
  // Cancel still refunds
}

class DispensingState implements MachineState {
  // Most operations rejected during physical dispense
  // After dispense, transition to RETURNING_CHANGE or IDLE
}

The VendingMachine class

class VendingMachine {
  private MachineState state;
  private Inventory inventory;
  private CoinInventory coinInventory;  // tracks coins/notes available for change
  private BigDecimal insertedAmount;
  private Item selectedItem;
  
  public void insertMoney(Money money) { state.insertMoney(this, money); }
  public void selectItem(String itemId) { state.selectItem(this, itemId); }
  public void cancel() { state.cancel(this); }
  
  void transitionTo(MachineState newState) { this.state = newState; }
  
  void dispenseSelectedItem() {
    BigDecimal change = insertedAmount.subtract(selectedItem.price);
    // Physically dispense the item
    inventory.removeItem(selectedItem.id);
    coinInventory.addAmount(selectedItem.price);  // money goes into machine
    if (change.signum() > 0) {
      List coinsForChange = coinInventory.makeChange(change);
      // Physically dispense change
    }
    // Reset
    insertedAmount = BigDecimal.ZERO;
    selectedItem = null;
    transitionTo(new IdleState());
  }
}

Cross-examination round 1: change-making

Interviewer: I insert ₹100, item costs ₹40, change is ₹60. Machine has 5 × ₹10 = ₹50, 0 × ₹20, 2 × ₹50 = ₹100 in stock. Can it make change?
Tricky. Greedy says: try largest first → ₹50 (have 2); after one ₹50, need ₹10 more. Have 5 × ₹10. So: 1 × ₹50 + 1 × ₹10 = ₹60. Yes. The algorithm tries each denomination greedily. But: with non-canonical coin systems (rare in real currency), greedy may fail and DP is needed. For Indian or US currency, greedy works.
Follow-up: What if machine has 1 × ₹50 and 1 × ₹10 only (₹60 total)? Greedy works. What if 0 × ₹50, 1 × ₹20, 4 × ₹10? Greedy: try ₹50 (none), ₹20 (1 → ₹40 remaining? Wait need ₹60 total, used ₹20, need ₹40), then ₹10 × 4 = ₹40. Works: 1 × ₹20 + 4 × ₹10 = ₹60. Greedy correct for these denominations.

Takeaway

Vending machines are the State pattern's textbook example. Each state defines its allowed operations and transitions. Adding new operations or states is localised. This is far cleaner than a giant switch on a state enum inside one class.