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
| Entity | Responsibility |
|---|---|
VendingMachine | Main context object that owns state, inventory, balance, and selected slot. |
VendingMachineState | Contract for allowed actions in each state. |
IdleState | Initial state; accepts the first note. |
HasMoneyState | Allows more money, item selection, and change return. |
DispensingState | Dispenses item and blocks other user actions. |
Inventory | Stores slots by code. |
Slot | Stores item and quantity. |
Item | Stores name and price. |
State Design
The key decision is to model the machine as states instead of writing large if blocks inside every method.
| State | Insert Note | Select Item | Dispense | Return Change |
|---|---|---|---|---|
IdleState | Move to HasMoneyState | Reject | Reject | Reject |
HasMoneyState | Add balance | Validate and move to DispensingState | Reject | Return balance and reset |
DispensingState | Reject | Reject | Dispense item and reset | Reject |
Main Flow
- Machine starts in
IdleState. - User inserts money and machine moves to
HasMoneyState. - User selects a slot.
- Machine validates slot, quantity, and balance.
- Machine moves to
DispensingState. - 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.
VendingMachineshould 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
OutOfServiceStateorMaintenanceState.
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.