LLD Question 02

Vending Machine

Design a vending machine with inventory, payments, item selection, dispensing, and change return.

Learning note

State pattern keeps invalid actions local to the current machine state instead of scattering conditionals everywhere.

Problem Statement

Design a vending machine that accepts notes, lets the user select an item, dispenses the item, and returns change when needed.

Functional Requirements

  • The machine should maintain inventory by slot code.
  • A user can insert one or more notes.
  • A user can select an item by slot code.
  • The machine should reject invalid or empty slots.
  • The machine should ask for more money if the balance is insufficient.
  • The machine should dispense the item and return change after a successful purchase.

Core Entities

EntityResponsibility
VendingMachineMain context object that owns state, inventory, balance, and selected slot.
VendingMachineStateContract for allowed actions in each state.
IdleStateInitial state; accepts the first note.
HasMoneyStateAllows more money, item selection, and change return.
DispensingStateDispenses item and blocks other user actions.
InventoryStores slots by code.
SlotStores item and quantity.
ItemStores name and price.

State Design

The key decision is to model the machine as states instead of writing large if blocks inside every method.

StateInsert NoteSelect ItemDispenseReturn Change
IdleStateMove to HasMoneyStateRejectRejectReject
HasMoneyStateAdd balanceValidate and move to DispensingStateRejectReturn balance and reset
DispensingStateRejectRejectDispense item and resetReject

Main Flow

  1. Machine starts in IdleState.
  2. User inserts money and machine moves to HasMoneyState.
  3. User selects a slot.
  4. Machine validates slot, quantity, and balance.
  5. Machine moves to DispensingState.
  6. Item is dispensed, change is returned, balance is reset, and state returns to idle.

Complete Code: Bottom-Up

Read this section from the smallest domain objects first and then move upward. The implementation ends at VendingMachine, where inventory, money, selected slot, and states converge, followed by Main as the demo entry point.

1. Note.java

public enum Note {
    TEN(10), TWENTY(20), FIFTY(50), HUNDRED(100);
 
    private final int value;
 
    Note(int value) { this.value = value; }
    int getValue()  {  return value; }
}

2. Item.java

public class Item {
    private final String name;
    private final int price;
 
    Item(String name, int price){
        this.name = name;
        this.price = price;
    }
 
    public String getName() {
        return name;
    }
 
    public int getPrice() {
        return price;
    }
}

3. Slot.java

public class Slot {
    private final Item item;
    private int qty;
 
    Slot(Item item, int qty){
        this.item = item;
        this.qty = qty;
    }
 
    void dispense(){
        if(isEmpty())    throw new RuntimeException("Not Enough quantity to dispense");
        qty--;
    }
 
    void restock(int cnt){
        if(cnt < 0)     throw new RuntimeException("Can't restock negative quantity");
        qty += cnt;
    }
 
    public Item getItem(){
        return item;
    }
 
    public int getQty(){
        return qty;
    }
 
    public boolean isEmpty(){
        return qty == 0;
    }
}

4. Inventory.java

import java.util.LinkedHashMap;
import java.util.Map;
 
public class Inventory {
    private final Map<String, Slot>  slots = new LinkedHashMap<>();
 
    public void addSlot(String code, Slot slot){
        slots.put(code.toUpperCase(), slot);
    }
 
    public Slot getSlot(String code){
        return slots.get(code.toUpperCase());
    }
 
    public boolean hasSlot(String code){
        return slots.containsKey(code.toUpperCase());
    }
 
    public void display(){
        for(String slotId: slots.keySet()){
            System.out.println("Slot: " + slotId + " Item: " + slots.get(slotId).getItem()
            + " Qty: " + slots.get(slotId).getQty());
        }
    }
}

5. VendingMachineState.java

public interface VendingMachineState {
    void insertNote(Note note);
    void selectItem(String code);
    void dispenseItem();
    void returnChange();
}

6. IdleState.java

public class IdleState implements VendingMachineState{
    private final VendingMachine machine;
 
    IdleState(VendingMachine machine){
        this.machine = machine;
    }
 
    @Override
    public void insertNote(Note note) {
        machine.addBalance(note.getValue());
        machine.setCurrentState(machine.getHasMoneyState());
        System.out.println("You have inserted: " + note.getValue() + " Current Balance: " + machine.getCurrentBalance());
    }
 
    @Override
    public void selectItem(String code) {
        System.out.println("Please Insert Note First!");
    }
 
    @Override
    public void dispenseItem() {
        System.out.println("Please Insert Note First!");
    }
 
    @Override
    public void returnChange() {
        System.out.println("Please Insert Note First!");
    }
}

7. HasMoneyState.java

public class HasMoneyState implements VendingMachineState{
    private final VendingMachine machine;
 
    HasMoneyState(VendingMachine machine){
        this.machine = machine;
    }
 
    @Override
    public void insertNote(Note note) {
        machine.addBalance(note.getValue());
        System.out.println("You have inserted: " + note.getValue() + " Current Balance: " + machine.getCurrentBalance());
    }
 
    @Override
    public void selectItem(String code) {
        Inventory inv = machine.getInventory();
        if(!inv.hasSlot(code))
            throw new IllegalStateException("The Entered Slot is not present!");
 
        Slot slot = inv.getSlot(code);
        if(slot.isEmpty())
            throw new IllegalStateException("The entered Slot is empty!");
 
        Item item = slot.getItem();
        if(machine.getCurrentBalance() < item.getPrice()){
            System.out.println("The Selected Item price is " + item.getPrice() + " Current Balance is: "
                    + machine.getCurrentBalance() + " " + (item.getPrice() - machine.getCurrentBalance()) + " Needed More!");
            return;
        }
 
        machine.setCurrentState(machine.getDispensingState());
        machine.setSelectedSlot(code);
        machine.dispenseItem();
    }
 
    @Override
    public void dispenseItem() {
        System.out.println("Please Select the Item First!");
    }
 
    @Override
    public void returnChange() {
        System.out.println("<---- Please Collect the change: " + machine.getCurrentBalance());
        machine.resetBalance();
        machine.setCurrentState(machine.getIdleState());
    }
}

8. DispensingState.java

public class DispensingState implements VendingMachineState{
    private VendingMachine machine;
 
    DispensingState(VendingMachine machine){
        this.machine = machine;
    }
 
    @Override
    public void insertNote(Note note) {
        System.out.println("Please Wait, Dispensing Item!");
    }
 
    @Override
    public void selectItem(String code) {
        System.out.println("Please Wait, Dispensing Item!");
    }
 
    @Override
    public void dispenseItem() {
        String slotId = machine.getSelectedSlot();
        Slot slot = machine.getInventory().getSlot(slotId);
 
        int price = slot.getItem().getPrice();
        machine.deductBalance(price);
        slot.dispense();
        System.out.println("Item: " + slot.getItem().getName() + " Dispensed!");
 
        if(machine.getCurrentBalance() > 0)
            System.out.println("<--- Please Collect the change: " + machine.getCurrentBalance());
 
        machine.resetBalance();
        machine.setSelectedSlot(null);
        machine.setCurrentState(machine.getIdleState());
    }
 
    @Override
    public void returnChange() {
        System.out.println("Please Wait, Dispensing Item!");
    }
}

9. VendingMachine.java

public class VendingMachine {
    private static VendingMachine instance;
 
    public static VendingMachine getInstance(){
        if(instance == null)
            instance = new VendingMachine();
        return instance;
    }
 
    //State Interfaces
    private final VendingMachineState idleState;
    private final VendingMachineState hasMoneyState;
    private final VendingMachineState dispensingState;
    private VendingMachineState currentState;
 
    //Core Field
    private final Inventory inventory;
    private String selectedSlot;
    private int balance;
 
    VendingMachine(){
        inventory = new Inventory();
        idleState = new IdleState(this);
        hasMoneyState = new HasMoneyState(this);
        dispensingState = new DispensingState(this);
        currentState = idleState;
    }
 
    public void addItem(String code, Item item, int qty){
        inventory.addSlot(code, new Slot(item, qty));
    }
 
    public void restockItem(String code, int qty){
        inventory.getSlot(code).restock(qty);
    }
 
    public void insertNote(Note note) { currentState.insertNote(note); }
    public void selectItem(String code) { currentState.selectItem(code); }
    public void dispenseItem()  { currentState.dispenseItem(); }
    public void returnChange()  { currentState.returnChange(); }
 
 
    public int getCurrentBalance(){
        return balance;
    }
    public void deductBalance(int val){
        this.balance -= val;
    }
    public void resetBalance(){
        this.balance = 0;
    }
    public void addBalance(int val){
        this.balance += val;
    }
 
    public VendingMachineState getIdleState(){
        return idleState;
    }
    public VendingMachineState getHasMoneyState(){
        return hasMoneyState;
    }
    public VendingMachineState getDispensingState(){
        return dispensingState;
    }
    public VendingMachineState getCurrentState(){
        return  currentState;
    }
    public void setCurrentState(VendingMachineState state){
        this.currentState = state;
    }
 
    public void setSelectedSlot(String code){
        this.selectedSlot = code;
    }
    public String getSelectedSlot(){
        return selectedSlot;
    }
 
    public Inventory getInventory(){
        return inventory;
    }
}

10. Main.java

public class Main {
    public static void main(String[] args) {
        VendingMachine machine = VendingMachine.getInstance();
 
        machine.addItem("A1", new Item("Diet Coke", 40), 10);
        machine.addItem("A2", new Item("Kurkure", 10), 15);
        machine.addItem("B1", new Item("Lays", 20), 10);
        machine.addItem("B2", new Item("Biscuits", 50), 10);
 
        machine.insertNote(Note.TEN);
        machine.selectItem("A2");
 
        machine.insertNote(Note.FIFTY);
        machine.insertNote(Note.TWENTY);
        machine.selectItem("B2");
 
        machine.insertNote(Note.TEN);
        machine.selectItem("A1");
        machine.returnChange();
    }
}

What I Learned

  • State pattern is a strong fit when the same action behaves differently depending on context.
  • VendingMachine should delegate behavior to the current state instead of owning every conditional.
  • Inventory and slot logic should stay separate from payment and state transition logic.
  • The design is easy to extend with more states like OutOfServiceState or MaintenanceState.

Possible Improvements

  • Make getInstance() thread-safe if this becomes a concurrent application.
  • Add payment abstraction for coins, notes, cards, and UPI.
  • Add refund flow when dispensing fails.
  • Add tests for invalid state transitions and insufficient balance.