SevyDevy

Guide · System design

LLD: Design a Vending Machine with the State Pattern

20 min readUpdated By Saket Bhatnagar

Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.

In short

  • Vending machine LLD (low level design) is the class design for the machine's software. Its core is the state pattern: each situation the machine can be in, such as Idle or Dispensing, becomes its own class.
  • One machine object, called the context, holds exactly one current state and hands every button press to it. That state object decides what the press means right now.
  • The classic vending bugs are a press at the wrong moment, like cancel while the motor turns. The state pattern gives each of those moments one small, testable method.
  • Money needs more than states. Keep rupees in an int, not a double, and save each sale to memory that survives a power cut. Before dispensing, check that the change tubes, the coin stacks that hold change, can pay it.
  • Beginners can stop after running the Java demo. Engineers get the rest: change making with near-empty coin tubes, which power cuts still lose money and why, and one event queue that handles presses one at a time.

What goes wrong when a vending machine runs on if-else flags?

The machine misbehaves when a button is pressed at the wrong moment, because one long method has to guess what the press means. A good vending machine LLD removes that guessing.

Picture a common setup. A small company runs drink and snack machines in Bengaluru college corridors and bus stands. The setup is made up, but the three bugs below are the classic ones.

The team's first firmware, the software that runs inside the machine, had one class with three true or false flags: hasMoney, isDispensing and isOutOfService. Every button press went through one long method full of if and else checks.

It worked for months. Then three kinds of complaint reached the support inbox.

  • A student pressed cancel while the spiral motor, the coil that pushes a packet forward, was turning. She got her Rs 35 juice and her Rs 35 back.
  • A commuter dropped a Rs 10 coin in during dispensing. The machine kept the coin and gave no credit for it.
  • Power failed in the middle of a sale. After the restart, the customer's Rs 20 credit had vanished, with no record of it.

Each bug could be patched with one more flag. But three flags already allow 8 mixes, and each new flag doubles that count. Most mixes mean nothing, and nobody writes down what they should do.

This guide rebuilds that firmware around the state pattern. It follows the same machine from the first list of states to change making, refunds and power cuts.

What is vending machine LLD, and how do you start one?

Vending machine LLD is the class design of the machine's software: which classes exist, what each one does and how they call each other. You start by listing what the machine must do, then naming its states.

LLD sits below system design, which picks servers, databases and networks. An LLD answer works with class names and method signatures in any object-oriented language. This guide uses Java.

The team starts with the requirements, the plain list of what the machine must do.

  • Accept Rs 5, 10 and 20 coins and notes from Rs 10 to Rs 100.
  • Later, accept UPI (Unified Payments Interface), India's instant bank-to-bank payment system, through a QR code on the screen.
  • Let the customer pick a slot, such as B3, and show its price.
  • Dispense only when the credit covers the price and the machine can pay the change.
  • Refund the full credit when the customer presses cancel before dispensing starts.
  • Survive a power cut without losing anyone's money or giving away free items.

The team also writes down what is out of scope: the cloud dashboard, restocking routes and price changes. Saying this out loud in an interview keeps the problem small enough to finish.

Then come the classes. VendingMachine is the context, the object that owns the current state and passes every event to it. MachineState is an interface, a list of methods that every state class must provide.

Each state, such as IdleState, implements that interface. Inventory maps each slot to an item and a stock count. CoinBox tracks how many coins are left in each change tube, the stacks the machine pays change from.

Here is the class map. Start at VendingMachine and notice that it never decides what a button means. It only delegates, meaning it hands the work to its current state.

Vending machine classes

The context owns the data: credit, stock and coins. The five state classes own the rules for every button. UPI arrives later as a sixth state.

Diagram connections:

  • VendingMachine to MachineState (Delegates every event)
  • MachineState to IdleState (Implemented by)
  • MachineState to HasMoneyState (Implemented by)
  • MachineState to DispensingState (Implemented by)
  • MachineState to ReturningMoneyState (Implemented by)
  • MachineState to OutOfServiceState (Implemented by)
  • VendingMachine to Inventory (Reads and updates stock)
  • VendingMachine to CoinBox (Pays change)
  • UPI payment client to VendingMachine (Payment confirmed)
  • VendingMachine: Context: holds current state and credit
  • MachineState: Interface: one method per event
  • IdleState: Waits for money
  • HasMoneyState: Holds credit, takes a selection
  • DispensingState: Motor running, most presses refused
  • ReturningMoneyState: Paying change or a refund
  • OutOfServiceState: Fault: refuses money and selections
  • Inventory: Slot to item, price and count
  • CoinBox: Coins left in each change tube
  • UPI payment client: Later: confirms QR code payments

What are the states of a vending machine?

This design uses five states: Idle, HasMoney, Dispensing, ReturningMoney and OutOfService. A state is the situation the machine is in right now, and it decides what each button does.

A transition is a move from one state to another, caused by an event. An event is anything that happens to the machine, such as a coin dropping in, a key press or a sensor signal.

A front-load washing machine is a good picture. It is in exactly one stage at a time: wash, rinse or spin. On many models the door button works before the wash starts, but the door stays locked while the drum spins.

Map it part by part. The stage is the state. The door button is the cancel button. The locked door during the spin is the Dispensing state refusing cancel. Where the picture breaks: a washer moves on by its own timer, while a vending machine mostly waits for people.

Here are the five states. Follow the arrows out of Idle. Notice that Dispensing has no arrow labelled cancel: once the motor turns, cancel does nothing.

Vending machine states and transitions

The machine is in one box at a time, and each arrow is an event. Notice that every box, OutOfService included, has a way back to Idle.

Diagram connections:

  • Idle to HasMoney (Coin or note accepted)
  • HasMoney to Dispensing (Paid item chosen)
  • HasMoney to ReturningMoney (Cancel pressed)
  • Dispensing to ReturningMoney (Item dropped)
  • ReturningMoney to Idle (Money paid out)
  • HasMoney to OutOfService (Hardware fault)
  • OutOfService to Idle (Technician clears fault)
  • Idle: Shows prices, no credit yet
  • HasMoney: Credit on screen, more coins add to it
  • Dispensing: Motor turning, cancel does nothing
  • ReturningMoney: Pays change or a refund from the tubes
  • OutOfService: Refunds any credit, waits for a technician

Two moves are left off the picture to keep it readable. Adding more coins in HasMoney keeps the machine in HasMoney and raises the credit. A fault, such as a note jam, can send any state to OutOfService.

The best way to check your states is a table. Put the events down the side and the states across the top. Then fill every cell with what should happen.

What each event does in each state. The Java code below implements every cell.
EventIdleHasMoneyDispensingReturningMoneyOutOfService
Insert moneyAdd credit, go to HasMoneyAdd to the creditHand the money backHand the money backHand the money back
Select itemShow the priceCheck stock, credit and change, then dispenseShow Please waitShow Please waitShow Out of service
CancelNothing to refundRefund the credit, go to IdleShow Cannot cancel nowIgnoreIgnore
Item droppedLog a sensor faultLog a sensor faultCut the stock, pay change, go to IdleLog a sensor faultLog a sensor fault

Look at the Dispensing column. The first two bugs live there. The old code never had a written answer for cancel or a coin during dispensing, so it did whatever the flags happened to allow.

What is the state pattern, and why does a vending machine need it?

In the state pattern, an object changes its behaviour when its internal state changes, by handing each request to a separate state object. A vending machine needs it because every button means something different in each state.

A design pattern is a named, reusable shape for solving a common code problem. The state pattern is one of the 23 patterns in the Gang of Four book, Design Patterns. It is a behavioural pattern, one about how objects share work.

Without the pattern, every method asks which state the machine is in before doing anything. With it, each state class answers only for itself. Adding a new state means adding a class, not editing every if-else in the file.

Read the table above one column at a time. Each column becomes one class, and each row becomes one method in it. Five states and four events give five classes with four methods each, twenty small methods in all.

  • Each state's rules sit in one small class, so you can read and test them alone.
  • Illegal moves become visible, because every state must write something for every event.
  • A new state, such as a maintenance mode, is a new class instead of new flags.

The pattern has costs too. Twenty methods is more code than one switch. And a new event, unlike a new state, touches every state class, as the UPI section shows. For a machine with two states and one button, a plain if-else is clearer.

How do you implement the vending machine state pattern in Java?

You write a MachineState interface with one method per event, one class per state, and a VendingMachine class that forwards each event to its current state. The state classes call setState to move the machine on.

The code below is complete, not a sketch. Paste the five blocks, in order, into one file named Demo.java. Use Java 17 or newer. We ran it on OpenJDK 17.0.20 and 21.0.12, with the same output, as of 19 October 2026.

java
import java.util.*;

interface MachineState {
    void insertMoney(VendingMachine m, int rupees);
    void selectItem(VendingMachine m, String slot);
    void cancel(VendingMachine m);
    void itemDropped(VendingMachine m);
}

record Item(String name, int price) {}

class VendingMachine {
    private MachineState state = IdleState.INSTANCE;
    private int credit;                    // whole rupees, never a double
    private final Inventory inventory;
    private final CoinBox coinBox;

    VendingMachine(Inventory inventory, CoinBox coinBox) {
        this.inventory = inventory;
        this.coinBox = coinBox;
    }

    // Each button just hands the press to the current state.
    void insertMoney(int rupees) { state.insertMoney(this, rupees); }
    void selectItem(String slot) { state.selectItem(this, slot); }
    void cancel()                { state.cancel(this); }
    void itemDropped()           { state.itemDropped(this); }

    // Helpers the state classes call.
    void setState(MachineState next) {
        state = next;
        System.out.println("  state -> " + next.getClass().getSimpleName());
    }
    int credit() { return credit; }
    void addCredit(int rupees) { credit += rupees; show("Credit Rs " + credit); }
    Inventory inventory() { return inventory; }
    CoinBox coinBox() { return coinBox; }
    void show(String text) { System.out.println("  screen: " + text); }
    void reject(int rupees) { System.out.println("  handed back Rs " + rupees); }
    void startMotor(String slot) { System.out.println("  motor on, slot " + slot); }

    // Pays back credit minus what was spent, then moves to the next state.
    void payOut(int spent, MachineState next) {
        int amount = credit - spent;
        credit = 0;
        setState(ReturningMoneyState.INSTANCE);
        if (amount > 0) System.out.println("  paid Rs " + amount + ": " + coinBox.payOut(amount));
        setState(next);
    }

    // A hardware fault is handled the same way in every state.
    void fault(String reason) { show(reason); payOut(0, OutOfServiceState.INSTANCE); }
}

Two things matter here. The state field is the only place the machine remembers where it is, so there are no flags to get out of step. Each button method is one line, and it passes this, meaning the machine itself, so the state can read the credit and call setState.

Item is a Java record, a short class that only holds values. The payOut helper moves through ReturningMoney while coins are paid, then on to the next state. A hardware fault does the same thing in every state, so fault lives in the context instead of five copies.

The two states that make real decisions

HasMoneyState is the busiest state. Read its selectItem method from top to bottom: each check can stop the sale before anything moves.

java
final class HasMoneyState implements MachineState {
    static final HasMoneyState INSTANCE = new HasMoneyState();

    public void insertMoney(VendingMachine m, int rupees) { m.addCredit(rupees); }

    public void selectItem(VendingMachine m, String slot) {
        Item item = m.inventory().find(slot);
        if (item == null) { m.show("No such slot"); return; }
        if (m.inventory().count(slot) == 0) { m.show("Sold out"); return; }
        int due = item.price() - m.credit();
        if (due > 0) { m.show("Add Rs " + due); return; }
        if (!m.coinBox().canPay(-due)) { m.show("Exact amount only"); return; }
        m.setState(new DispensingState(slot, item));   // lock the buttons first
        m.startMotor(slot);                            // then turn the motor
    }

    public void cancel(VendingMachine m) { m.payOut(0, IdleState.INSTANCE); }
    public void itemDropped(VendingMachine m) { m.show("Sensor fault logged"); }
}

final class DispensingState implements MachineState {
    private final String slot;   // per-sale data, so a fresh object each sale
    private final Item item;

    DispensingState(String slot, Item item) { this.slot = slot; this.item = item; }

    public void insertMoney(VendingMachine m, int rupees) { m.reject(rupees); }
    public void selectItem(VendingMachine m, String s) { m.show("Please wait"); }
    public void cancel(VendingMachine m) { m.show("Cannot cancel now"); }

    public void itemDropped(VendingMachine m) {
        m.inventory().takeOne(slot);                  // count stock only on a real drop
        m.payOut(item.price(), IdleState.INSTANCE);   // change, if any, then Idle
    }
}

The checks run in a fixed order: no such slot, sold out, not enough money, then no change. The change check comes before the motor runs, because you cannot take a drink back. For a Rs 50 note against a Rs 35 juice, due is minus 15, so the change is Rs 15.

The free juice bug is gone because cancel now lands on DispensingState, whose cancel only shows a message. Its insertMoney hands the coin straight back, which fixes the swallowed coin. Both fixes rely on events being handled one at a time, which a later section sets up.

Locking first, setState before startMotor, is a cheap habit on top of that. Any event raised during the motor call already sees DispensingState.

The three simple states

The other three states mostly refuse things. None of them has fields, and neither does HasMoneyState, so one shared object called INSTANCE is enough for each. DispensingState holds the slot and item for one sale, so it gets a fresh object every time.

java
final class IdleState implements MachineState {
    static final IdleState INSTANCE = new IdleState();

    public void insertMoney(VendingMachine m, int rupees) {
        m.setState(HasMoneyState.INSTANCE);
        m.addCredit(rupees);
    }
    public void selectItem(VendingMachine m, String slot) {
        Item item = m.inventory().find(slot);
        m.show(item == null ? "No such slot" : item.name() + ", Rs " + item.price());
    }
    public void cancel(VendingMachine m) { }   // nothing to refund
    public void itemDropped(VendingMachine m) { m.show("Sensor fault logged"); }
}

final class ReturningMoneyState implements MachineState {
    static final ReturningMoneyState INSTANCE = new ReturningMoneyState();

    public void insertMoney(VendingMachine m, int rupees) { m.reject(rupees); }
    public void selectItem(VendingMachine m, String slot) { m.show("Please wait"); }
    public void cancel(VendingMachine m) { }
    public void itemDropped(VendingMachine m) { m.show("Sensor fault logged"); }
}

final class OutOfServiceState implements MachineState {
    static final OutOfServiceState INSTANCE = new OutOfServiceState();

    public void insertMoney(VendingMachine m, int rupees) { m.reject(rupees); }
    public void selectItem(VendingMachine m, String slot) { m.show("Out of service"); }
    public void cancel(VendingMachine m) { }
    public void itemDropped(VendingMachine m) { m.show("Sensor fault logged"); }
}

Stock and change

Inventory keeps the item and the count for each slot. CoinBox keeps the count in each change tube, and its plan method picks the coins. The change section below explains why plan is a loop and not one greedy pass.

java
class Inventory {
    private final Map<String, Item> items = new HashMap<>();
    private final Map<String, Integer> counts = new HashMap<>();

    void load(String slot, Item item, int count) { items.put(slot, item); counts.put(slot, count); }
    Item find(String slot) { return items.get(slot); }
    int count(String slot) { return counts.getOrDefault(slot, 0); }
    void takeOne(String slot) { counts.merge(slot, -1, Integer::sum); }
}

class CoinBox {
    private int ones, twos, fives;   // coins left in each change tube

    CoinBox(int ones, int twos, int fives) { this.ones = ones; this.twos = twos; this.fives = fives; }

    // Coins {fives, twos, ones} that make the amount, or null if the tubes cannot.
    int[] plan(int amount) {
        for (int f = Math.min(fives, amount / 5); f >= 0; f--) {
            int rest = amount - 5 * f;
            int t = Math.min(twos, rest / 2);   // as many Rs 2 coins as fit
            int o = rest - 2 * t;               // the rest in Rs 1 coins
            if (o <= ones) return new int[] {f, t, o};
        }
        return null;
    }

    boolean canPay(int amount) { return plan(amount) != null; }

    String payOut(int amount) {
        int[] p = plan(amount);
        if (p == null) return "tubes too low, owed amount logged";
        fives -= p[0]; twos -= p[1]; ones -= p[2];
        return p[0] + " x Rs 5, " + p[1] + " x Rs 2, " + p[2] + " x Rs 1";
    }
}

Run it and check the output

The last block drives one sale, then replays the two old bugs, a sold-out slot and a tricky change amount.

java
public class Demo {
    public static void main(String[] args) {
        Inventory stock = new Inventory();
        stock.load("B3", new Item("Mango juice", 35), 1);
        VendingMachine m = new VendingMachine(stock, new CoinBox(5, 5, 10));

        System.out.println("1. Rs 50 note, then slot B3");
        m.insertMoney(50);
        m.selectItem("B3");
        System.out.println("2. Cancel and a Rs 10 coin while the motor turns");
        m.cancel();
        m.insertMoney(10);
        System.out.println("3. The drop sensor fires");
        m.itemDropped();
        System.out.println("4. Rs 20 coin, slot B3 again, then cancel");
        m.insertMoney(20);
        m.selectItem("B3");
        m.cancel();
        System.out.println("5. Rs 6 change with no Rs 1 coins: "
                + Arrays.toString(new CoinBox(0, 3, 4).plan(6)));
    }
}

Compile with javac, the Java compiler, then run with java. Running java Demo.java directly fails on Java 21 with a missing main error, because the source launcher looks for main in the first class in the file, MachineState.

bash
javac Demo.java
java Demo
text
1. Rs 50 note, then slot B3
  state -> HasMoneyState
  screen: Credit Rs 50
  state -> DispensingState
  motor on, slot B3
2. Cancel and a Rs 10 coin while the motor turns
  screen: Cannot cancel now
  handed back Rs 10
3. The drop sensor fires
  state -> ReturningMoneyState
  paid Rs 15: 3 x Rs 5, 0 x Rs 2, 0 x Rs 1
  state -> IdleState
4. Rs 20 coin, slot B3 again, then cancel
  state -> HasMoneyState
  screen: Credit Rs 20
  screen: Sold out
  state -> ReturningMoneyState
  paid Rs 20: 4 x Rs 5, 0 x Rs 2, 0 x Rs 1
  state -> IdleState
5. Rs 6 change with no Rs 1 coins: [0, 3, 0]

What just happened? Step 1 moved the machine from Idle to HasMoney to Dispensing. In step 2, cancel showed Cannot cancel now and the Rs 10 coin came straight back, so both old bugs are gone.

Step 3 cut the stock only when the drop was seen, then paid Rs 15 as three Rs 5 coins. Step 4 found B3 sold out and refunded the full Rs 20. Step 5 found Rs 6 in three Rs 2 coins although the Rs 1 tube was empty.

If you are new to LLD, this is a good place to stop. You have the states, the pattern and code that runs. The rest is for engineers: enums against classes, change with near-empty tubes, power cuts and races.

Should vending machine states be an enum or separate classes?

Use separate classes when states carry logic or per-sale data. Use an enum when the states are few, fixed and hold no data. A sealed interface is a third option, and all three beat flags scattered across one class.

A Java enum is a type with a fixed set of named constants, and Java makes no other instances. Writing new on an enum fails to compile. javac 21 says: enum classes may not be instantiated. javac 17 says enum types instead of enum classes.

Each enum constant can have its own body that overrides a method, so IDLE, HAS_MONEY and DISPENSING could each answer insertMoney differently. The limit is that every constant is one shared object, so it is a poor place for per-sale data like the slot being dispensed.

Newer Java adds a third route. Since Java 17, a sealed interface lists exactly which classes may implement it. Since Java 21, a switch over a sealed type that covers every class needs no default branch, the catch-all case, and the compiler checks that none is missing.

java
sealed interface State permits Idle, HasMoney, Dispensing {}
record Idle() implements State {}
record HasMoney(int credit) implements State {}
record Dispensing(String slot) implements State {}

class Sealed {
    static String onCancel(State s) {
        return switch (s) {                 // no default branch
            case Idle i -> "nothing to refund";
            case HasMoney h -> "refund Rs " + h.credit();
            case Dispensing d -> "cannot cancel now";
        };
    }
    public static void main(String[] a) {
        System.out.println(onCancel(new HasMoney(50)));
        System.out.println(onCancel(new Dispensing("B3")));
    }
}

You can check the compiler's promise yourself. Save this as Sealed.java in its own folder, then run javac Sealed.java and java Sealed. It prints refund Rs 50 and cannot cancel now. Then declare a fourth record, AwaitingPayment, add it to the permits list, and leave the switch alone. javac 21 stops with: the switch expression does not cover all possible input values.

Compiled for Java 17, the same file fails, because pattern matching for switch, case labels that test an object's type, was only finalised in Java 21. javac 21 with --release 17, the flag that compiles for Java 17, says: patterns in switch statements are not supported in -source 17. A real javac 17 says they are a preview feature, disabled by default.

Four ways to model vending machine states in Java
ApproachGood forWeak spot
Flags with if-elseTwo states and one buttonFlag mixes nobody planned, like the free juice bug
Enum with a body per constantFew fixed states that hold no dataConstants are shared, so no per-sale data
One class per stateStates with logic, data and their own testsMore classes, and a new event touches all of them
Sealed interface with switchAll rules for one event in one place, checked by the compilerA new state means editing every switch, though javac lists them

This design uses classes because DispensingState holds per-sale data, and because each state's tests can then run alone.

How does a vending machine give change with limited coins?

It picks coins from its change tubes, and it checks that it can pay before it dispenses. The hard part is that tubes run low, so the obvious method can fail when a correct answer exists.

The machines here pay change from three tubes holding Rs 1, Rs 2 and Rs 5 coins. Customers never insert Rs 1 or Rs 2 coins, so only a technician refills those two tubes, and they run low between visits. The simplest method is greedy: always take the biggest coin that still fits the amount left.

Greedy breaks when a tube runs empty. Say the change due is Rs 6, the Rs 1 tube is empty, and the Rs 2 tube has three coins. Greedy takes a Rs 5, then needs a Rs 1 it does not have.

Three Rs 2 coins would have worked. So plan tries each possible number of Rs 5 coins, from most to fewest. For each try, it takes as many Rs 2 coins as fit, then fills the rest with Rs 1 coins.

Taking as many Rs 2 coins as fit is safe, because using fewer would only need more Rs 1 coins. That shortcut relies on the smallest coin being Rs 1. For other coin sets, search all mixes instead.

We checked plan against a brute-force search of every coin mix: 44,469 cases, with up to 8 coins per tube and amounts up to Rs 60. They agreed every time. Plain greedy failed in 476 of those cases where change was possible.

When plan returns null, meaning no mix of coins works, the screen shows Exact amount only. That is still the HasMoney state. Nothing moves and the credit stays, so the customer can pick another item or press cancel.

  • Sold out: the slot's count is zero, so the screen says Sold out and the credit stays for another choice.
  • Not enough credit: the screen shows how much more to add, and the state does not change.
  • Note jam: the note reader reports a fault, so fault refunds any credit and moves to OutOfService.
  • Fault while the motor turns: the demo's fault refunds the full credit, even though the packet may still drop. A real machine should wait for the drop sensor, or log the sale for a human.
  • Item stuck in the spiral: the drop sensor across the chute sees nothing. The team's rule is to retry once, then refund and mark the slot faulty. The demo leaves this timer out.
  • Tubes too low for a refund: the demo's payOut only logs what is owed. A real machine needs a rule here too, such as refusing new money while the tubes are low.

What happens when a vending machine loses power mid-sale?

A careful machine saves each sale to memory that survives power loss, before every risky step. After a restart, that record decides whether to finish or refund the sale. A few cut points still leave real doubt, and the design must choose who carries it.

The team saves a small record in flash memory, storage on the board that keeps data without power. Before the motor starts, it writes the sale id, slot B3, the Rs 50 credit, the Rs 35 price and the state Dispensing. It also saves the credit each time it changes, so a cut in HasMoney can be refunded.

The record itself needs a guard, because a write cut halfway leaves a half-written record. This design keeps two copies, each with a checksum, a small number computed from the data. On restart it trusts the newest copy whose checksum matches.

Databases use the same write-ahead idea. PostgreSQL's documentation states it plainly: changes to data files are written only after the changes have been logged. Write down what you are about to do, then do it.

Follow the questions in the diagram from top to bottom.

Recovering a sale after a power cut

The saved record decides the next step. Notice that the payout question comes first: once coins may have moved, the machine stops guessing.

Diagram connections:

  • Power comes back to Unfinished sale saved?
  • Unfinished sale saved? to Start in Idle (No)
  • Unfinished sale saved? to Payout already started? (Yes)
  • Payout already started? to Log it for a human (Yes)
  • Payout already started? to Drop logged? (No)
  • Drop logged? to Pay change, mark done (Yes)
  • Drop logged? to Refund credit, mark done (No)
  • Log it for a human to Start in Idle
  • Pay change, mark done to Start in Idle
  • Refund credit, mark done to Start in Idle
  • Power comes back: Firmware restarts and reads flash memory
  • Unfinished sale saved?: A record without a done mark
  • Start in Idle: Ready for the next customer
  • Payout already started?: Coins may already have left a tube
  • Log it for a human: Never pay twice; support settles it
  • Drop logged?: The sensor event is saved too
  • Pay change, mark done: Rs 15 back for a Rs 35 juice
  • Refund credit, mark done: Rs 50 back, the sale never happened

The payout question is about idempotency: doing something twice should have the same effect as doing it once. A coin payout is not naturally idempotent, because coins that left the tube cannot be taken back.

So the machine writes payout started before paying and done after. If power dies between those two writes, the restart cannot know whether coins came out. It logs the sale for a human instead of paying again.

We simulated a cut after every step of the Rs 50 sale. With only a done mark, a cut between paying the Rs 15 change and writing done paid the change twice. With the payout started mark, those cuts went to a human, and nothing was paid twice.

One gap stays open in both designs. If the packet falls but power dies before the drop is saved, the restart refunds Rs 50 and the customer keeps the item. The team accepts that rare loss, because the other choice keeps a customer's money for nothing.

Where does UPI fit?

UPI adds a sixth state, AwaitingPayment. The machine shows a QR code and waits for the team's payment server to confirm the money arrived. In this design it gives up after 90 seconds, a made-up timeout, and returns to Idle.

A late confirmation is the tricky case, because the customer may have walked away. The server must then refund through the payment provider, not through the coin tubes.

The new state is one class. The new event that starts it, startUpi, is a new interface method, and javac then rejects all five existing state classes until each gets an answer, usually Ignore. New states are cheap; new events touch every state.

A Java default method on the interface, a method with a body that classes inherit, avoids those five edits. It also hides cells you forgot to decide, so use it with care.

Who handles events that arrive at the same moment?

Events come from many parts at once: the coin changer, the keypad, the drop sensor and the network. A thread is one line of work inside a program, and two threads can run at the same moment. If two threads handle two events together, both can read HasMoney and both can start the motor.

We forced exactly that timing on the demo code with two threads and one Rs 50 credit. The motor started twice. With one queue and one worker thread, the second press got Please wait instead.

So every event goes on one queue, and a single thread takes them out in order. In Java, Executors.newSingleThreadExecutor gives you this: its documentation says tasks run one after another, never two at once.

Inside many machines, a controller talks to the coin changer, note reader and card reader over MDB (Multi-Drop Bus). A bus is one shared link that several devices talk over, and MDB is a vending industry standard for it, published by NAMA, the US vending trade association. The controller is the single master, and each device answers when addressed.

Here is one sale, event by event: a Rs 50 note for a Rs 35 juice. Each box is one message, and the note says which part talks to which.

One sale, event by event

Each step is one event on one queue, handled by whichever state is current. Notice the three saves: before the motor, before any coin moves, and after.

Diagram connections:

  • Rs 50 note accepted to Credit shown, HasMoney
  • Credit shown, HasMoney to Slot B3 chosen
  • Slot B3 chosen to Sale record saved
  • Sale record saved to Spiral motor turns
  • Spiral motor turns to Drop seen
  • Drop seen to Drop and payout start saved
  • Drop and payout start saved to Rs 15 change paid
  • Rs 15 change paid to Done mark saved
  • Rs 50 note accepted: Note reader → Controller
  • Credit shown, HasMoney: Controller → Display
  • Slot B3 chosen: Keypad → Controller
  • Sale record saved: Controller → Flash memory
  • Spiral motor turns: Controller → Motor
  • Drop seen: Drop sensor → Controller
  • Drop and payout start saved: Controller → Flash memory
  • Rs 15 change paid: Controller → Coin changer
  • Done mark saved: Controller → Flash memory

How do you explain vending machine LLD in a system design interview?

Start from the states and the event table, then show the classes, then the money edge cases. I'd spend the first few minutes on states, because every later answer points back to them.

  1. Ask about scope

    Which payments: coins, notes, UPI or cards? One machine or a fleet? Is an admin or restock mode in scope?

  2. Draw the states

    Sketch Idle, HasMoney, Dispensing, ReturningMoney and OutOfService, with every arrow labelled by its event.

  3. Fill the event table

    Put events down the side and states across the top. Point at the cancel during dispensing cell and say what it does.

  4. Show the classes

    Name the context, the state interface, one class per state, Inventory and CoinBox. Explain that the context only delegates.

  5. Write two states in code

    HasMoney and Dispensing are the interesting ones. Show the check order, and why the change check runs before the motor.

  6. Cover the money edges

    Change with near-empty tubes, sold out, jams, power cuts and payouts that must never run twice. Then mention one queue for events.

Questions people ask

Which design pattern is used in a vending machine LLD?
The state pattern is the main one, because every button means something different in each state. Some designs add a strategy for change making, so the coin choice method can be swapped. A factory can create the right state objects. Lead with the state pattern, and add the others only if they solve a real problem in your design.
What is the difference between the state pattern and the strategy pattern?
They look alike in code, but they differ in who switches the object. In the strategy pattern, the caller picks a behaviour, such as a sorting method, and it usually stays put. In the state pattern, the state objects usually switch the machine themselves as events arrive, like HasMoney moving the machine to Dispensing.
Is a vending machine LLD the same as a finite state machine?
No, but the two are closely linked: a vending machine LLD is built around a finite state machine. A finite state machine is the model: a fixed set of states, the events that can happen, and the transitions between states. The state pattern is one way to code that model with objects. A transition table or a switch statement codes the same model. The event table in this guide is the model; the classes are one implementation of it.
How many states does a vending machine have?
There is no fixed number. This guide uses five: Idle, HasMoney, Dispensing, ReturningMoney and OutOfService. UPI or card payments add an AwaitingPayment state, and a restock mode could add a Maintenance state. Pick the smallest set where every cell of your event table has one clear answer.
Should vending machine state objects be singletons?
They can be when they hold no data. A singleton is a class with exactly one shared instance. In the demo, IdleState has no fields, so one shared INSTANCE is safe. DispensingState holds the slot and item for one sale, so it needs a new object each time. An enum gives you shared instances for free.
How do you test a vending machine built with the state pattern?
Test each state class alone, one test per cell of your event table. Create a machine, move it to Dispensing, press cancel, and check that no refund happened. Then add a few end-to-end runs like the demo, plus power-cut tests that restart from every saved record.

Sources and further reading

Practise this with Lala

Practise