Design a Vending Machine: State Pattern and Java Code<p>A vending machine has maybe twenty products and three buttons. How complicated can the design really be?</p><p>Quite complicated, once the same button is expected to behave differently depending on what happened five seconds earlier.</p><p>Press <code>A2</code> before inserting money: reject the selection.</p><p>Insert ₹50 and choose a ₹40 drink: dispense it and return ₹10.</p><p>Insert ₹20 for that same drink: keep the money, but do not dispense.</p><p>Press cancel halfway through: refund the current balance.</p><p>Try buying an empty slot: do not lose the customer's money.</p><p>The interesting part is no longer <code>Product</code> or <code>Inventory</code>.</p><p>It is <strong>state</strong>.</p><p>In LLD 01, our Parking Lot design was driven mostly by object responsibilities and replaceable strategies. This time we will solve a different problem: how do we make an object's legal behaviour depend on its current state without creating one enormous <code>if/else</code> block?</p><h2>Start With the Behaviour, Not the Classes</h2><p>For this version, our vending machine should support:</p><ul><li><p>products stored in coded slots such as <code>A1</code> and <code>B2</code>;</p></li><li><p>a price and available quantity for each product;</p></li><li><p>insertion of supported coin denominations;</p></li><li><p>selection of a product after money has been inserted;</p></li><li><p>rejection when the balance is insufficient;</p></li><li><p>dispensing when enough money is available;</p></li><li><p>return of remaining balance as change;</p></li><li><p>cancellation and refund;</p></li><li><p>prevention of two purchases from mutating the same machine simultaneously.</p></li></ul><p>We will leave a few things outside the first implementation:</p><ul><li><p>card and UPI payments;</p></li><li><p>remote inventory synchronization;</p></li><li><p>dynamic pricing;</p></li><li><p>hardware motor failures;</p></li><li><p>exact physical coin-change optimization;</p></li><li><p>operator authentication;</p></li><li><p>distributed vending-machine management.</p></li></ul><p>That last group is important. An LLD interview becomes messy very quickly if every possible real-world feature enters version one.</p><h2>The Tempting Design That Starts to Hurt</h2><p>A first attempt often looks like this:</p><pre><code>public void selectProduct(String code) { if (state == IDLE) { throw new IllegalStateException("Insert money first"); } else if (state == HAS_MONEY) { // validate product // validate balance // dispense } else if (state == DISPENSING) { throw new IllegalStateException("Please wait"); }}</code></pre><p>Then <code>insertCoin()</code> gets another state switch.</p><p>So does <code>cancel()</code>.</p><p>Soon we have:</p><pre><code>3 states × 3 operations = 9 behavioural combinations</code></pre><p>Add <code>OUT_OF_SERVICE</code>, <code>REFUNDING</code>, or card authorization later and the number of branches keeps growing.</p><p>Worse, the rules for one state are scattered across several methods.</p><p>A better question is:</p><blockquote><p>What if each state itself knew which operations were legal?</p></blockquote><p>That leads naturally to the <strong>State Pattern</strong>.</p><img src="https://media.hyive.com/users/f5853095-943d-4758-9e77-4b2200b54772/88a053f9-5498-4ed3-af48-1b0c5727bf9a.png" alt="image"><h2>Our State Machine</h2><p>We only need three states for the first design:</p><h3><code>IdleState</code></h3><p>There is no active transaction.</p><p>The machine can:</p><ul><li><p>accept money;</p></li><li><p>reject product purchase because there is no credit;</p></li><li><p>treat cancellation as a no-op.</p></li></ul><h3><code>HasCreditState</code></h3><p>A customer has inserted money.</p><p>The machine can:</p><ul><li><p>accept more money;</p></li><li><p>attempt a product selection;</p></li><li><p>cancel and refund.</p></li></ul><h3><code>DispensingState</code></h3><p>A successful purchase is being completed.</p><p>External actions should be rejected until the transaction finishes.</p><p>The happy path therefore looks like:</p><pre><code>IDLE ↓ insert coinHAS CREDIT ↓ select affordable productDISPENSING ↓ product + change returnedIDLE</code></pre><p>Cancellation gives us:</p><pre><code>HAS CREDIT ↓ cancelREFUND ↓IDLE</code></pre><p>We do not actually need a permanent <code>RefundState</code> yet because refunding is synchronous in this interview model.</p><h2>Model Money Without Floating-Point Arithmetic</h2><p>For currency, do not write:</p><pre><code>double price = 1.20;</code></pre><p>For this exercise, representing money as the smallest unit is cleaner.</p><p>So ₹40 becomes:</p><pre><code>4000 paise</code></pre><p>Our coin enum can therefore contain integer values:</p><pre><code>public enum Coin {
ONE_RUPEE(100), TWO_RUPEES(200), FIVE_RUPEES(500), TEN_RUPEES(1000), TWENTY_RUPEES(2000), FIFTY_RUPEES(5000);
private final int valueInPaise;
Coin(int valueInPaise) { this.valueInPaise = valueInPaise; }
public int valueInPaise() { return valueInPaise; }}</code></pre><p>For a production payment system I would normally introduce a proper <code>Money</code> value object with currency as well.</p><p>For one Indian vending machine, integer paise keeps the interview model focused.</p><h2>Product and Slot Have Different Responsibilities</h2><p>A product describes <strong>what is being sold</strong>:</p><pre><code>public record Product( String sku, String name, int priceInPaise) { public Product { if (priceInPaise <= 0) { throw new IllegalArgumentException( "Product price must be positive" ); } }}</code></pre><p>A slot describes <strong>where it is stocked and how much remains</strong>:</p><pre><code>public final class ProductSlot {
private final String code; private final Product product; private int quantity;
public ProductSlot( String code, Product product, int quantity) {
if (quantity < 0) { throw new IllegalArgumentException( "Quantity cannot be negative" ); }
this.code = code; this.product = product; this.quantity = quantity; }
public String getCode() { return code; }
public Product getProduct() { return product; }
public boolean isAvailable() { return quantity > 0; }
public void dispenseOne() { if (quantity <= 0) { throw new IllegalStateException( "Product is out of stock" ); }
quantity--; }
public int getQuantity() { return quantity; }}</code></pre><p>Keeping those concepts separate becomes useful when several machines or several slots eventually carry the same product.</p><h2>Inventory Owns Product Lookup</h2><p>The vending machine should not need to know whether inventory uses an array, map, database, or something else.</p><p>For now:</p><pre><code>public final class Inventory {
private final Map<String, ProductSlot> slots = new HashMap<>();
public void addSlot(ProductSlot slot) { if (slots.putIfAbsent(slot.getCode(), slot) != null) { throw new IllegalArgumentException( "Duplicate slot: " + slot.getCode() ); } }
public ProductSlot getSlot(String code) { ProductSlot slot = slots.get(code);
if (slot == null) { throw new IllegalArgumentException( "Unknown product slot: " + code ); }
return slot; }}</code></pre><p>There is still no database.</p><p>That is intentional.</p><p>Persistence is a storage concern; it is not required to understand the object model.</p><h2>Now Define the State Contract</h2><p>Every machine state must decide what happens when the same three commands arrive:</p><pre><code>public interface VendingState {
void insertCoin( VendingMachine machine, Coin coin );
DispenseResult selectProduct( VendingMachine machine, String slotCode );
int cancel( VendingMachine machine );
String name();}</code></pre><p>Notice what is absent:</p><pre><code>if state == ...switch state ...</code></pre><p>The machine delegates behaviour to the current state object.</p><h2>IdleState: Start a Transaction</h2><pre><code>public final class IdleState implements VendingState {
@Override public void insertCoin( VendingMachine machine, Coin coin) {
machine.addCredit(coin.valueInPaise()); machine.transitionTo(machine.hasCreditState()); }
@Override public DispenseResult selectProduct( VendingMachine machine, String slotCode) {
throw new IllegalStateException( "Insert money before selecting a product" ); }
@Override public int cancel(VendingMachine machine) { return 0; }
@Override public String name() { return "IDLE"; }}</code></pre><p>One successful <code>insertCoin()</code> changes both:</p><pre><code>balance: 0 → coin valuestate: IDLE → HAS_CREDIT</code></pre><h2>HasCreditState: Most of the Business Logic Lives Here</h2><p>This is where selection becomes meaningful.</p><pre><code>public final class HasCreditState implements VendingState {
@Override public void insertCoin( VendingMachine machine, Coin coin) {
machine.addCredit(coin.valueInPaise()); }
@Override public DispenseResult selectProduct( VendingMachine machine, String slotCode) {
ProductSlot slot = machine.inventory().getSlot(slotCode);
if (!slot.isAvailable()) { throw new IllegalStateException( "Selected product is out of stock" ); }
Product product = slot.getProduct();
if (machine.currentCredit() < product.priceInPaise()) {
int missing = product.priceInPaise() - machine.currentCredit();
throw new IllegalStateException( "Insufficient balance. Need " + missing + " more paise" ); }
machine.transitionTo( machine.dispensingState() );
return machine.completePurchase(slot); }
@Override public int cancel(VendingMachine machine) {
int refund = machine.takeAllCredit();
machine.transitionTo( machine.idleState() );
return refund; }
@Override public String name() { return "HAS_CREDIT"; }}</code></pre><p>There are two useful details here.</p><p>If stock is empty, the machine <strong>does not consume the customer's balance</strong>.</p><p>If the customer has insufficient money, the machine remains in <code>HAS_CREDIT</code>, so another coin can be inserted.</p><p>Those rules fall naturally out of the state model.</p><h2>DispensingState Rejects New Commands</h2><p>Dispensing is deliberately boring:</p><pre><code>public final class DispensingState implements VendingState {
private IllegalStateException busy() { return new IllegalStateException( "Machine is currently dispensing" ); }
@Override public void insertCoin( VendingMachine machine, Coin coin) { throw busy(); }
@Override public DispenseResult selectProduct( VendingMachine machine, String slotCode) { throw busy(); }
@Override public int cancel(VendingMachine machine) { throw busy(); }
@Override public String name() { return "DISPENSING"; }}</code></pre><p>Why model a state whose only job is to reject commands?</p><p>Because it makes the invariant explicit:</p><blockquote><p><strong>Once dispensing begins, a second user action must not modify the active transaction.</strong></p></blockquote><h2>Put the Transaction in VendingMachine</h2><p>Now the context becomes surprisingly small.</p><pre><code>public record DispenseResult( Product product, int changeInPaise) {}</code></pre><pre><code>public final class VendingMachine {
private final Inventory inventory;
private final VendingState idleState = new IdleState();
private final VendingState hasCreditState = new HasCreditState();
private final VendingState dispensingState = new DispensingState();
private VendingState state; private int currentCredit;
public VendingMachine(Inventory inventory) { this.inventory = inventory; this.state = idleState; }
public synchronized void insertCoin(Coin coin) { Objects.requireNonNull(coin); state.insertCoin(this, coin); }
public synchronized DispenseResult selectProduct( String slotCode) { return state.selectProduct(this, slotCode); }
public synchronized int cancel() { return state.cancel(this); }
Inventory inventory() { return inventory; }
int currentCredit() { return currentCredit; }
void addCredit(int amount) { currentCredit += amount; }
int takeAllCredit() { int amount = currentCredit; currentCredit = 0; return amount; }
DispenseResult completePurchase( ProductSlot slot) {
Product product = slot.getProduct();
if (state != dispensingState) { throw new IllegalStateException( "Machine is not dispensing" ); }
slot.dispenseOne();
int change = currentCredit - product.priceInPaise();
currentCredit = 0; state = idleState;
return new DispenseResult( product, change ); }
void transitionTo(VendingState nextState) { this.state = Objects.requireNonNull(nextState); }
VendingState idleState() { return idleState; }
VendingState hasCreditState() { return hasCreditState; }
VendingState dispensingState() { return dispensingState; }
public synchronized int getCurrentCredit() { return currentCredit; }
public synchronized String getStateName() { return state.name(); }}</code></pre><p>That is the complete core design.</p><p>The public API of the machine is only:</p><pre><code>insertCoin(...)selectProduct(...)cancel()</code></pre><p>Everything else exists to keep those operations correct.</p><img src="https://media.hyive.com/users/f5853095-943d-4758-9e77-4b2200b54772/ae2d26e7-2715-4d32-ab48-ee0e7e179638.png" alt="image"><h2>Walk Through an Actual Purchase</h2><p>Suppose slot <code>A1</code> contains Coke for ₹40.</p><p>The machine starts with:</p><pre><code>state = IDLEcredit = ₹0stock = 5</code></pre><p>Customer inserts ₹20:</p><pre><code>state = HAS_CREDITcredit = ₹20</code></pre><p>They insert another ₹20:</p><pre><code>state = HAS_CREDITcredit = ₹40</code></pre><p>They select <code>A1</code>.</p><p><code>HasCreditState</code> checks:</p><pre><code>Does A1 exist? yesIs stock available? yesIs credit >= price? yes</code></pre><p>The machine enters <code>DISPENSING</code>.</p><p>One unit is removed.</p><p>The transaction resets:</p><pre><code>state = IDLEcredit = ₹0stock = 4</code></pre><p>If the customer had inserted ₹50 instead, the same purchase would return ₹10 as change.</p><p>If they had inserted only ₹20, selection would fail but the transaction would remain active:</p><pre><code>state = HAS_CREDITcredit = ₹20</code></pre><p>That last detail is exactly why thinking in states is useful.</p><h2>What About Two People Pressing Buttons at Once?</h2><p>A physical vending machine normally represents one customer interaction at a time, but software events can still arrive concurrently.</p><p>Imagine:</p><pre><code>Thread A → select A1Thread B → cancel</code></pre><p>If both operations mutate the same balance and state without coordination, strange results become possible:</p><pre><code>product dispensed+full refund returned</code></pre><p>Our public mutation methods are therefore synchronized:</p><pre><code>public synchronized DispenseResult selectProduct(...)public synchronized int cancel()</code></pre><p>For one in-memory machine instance, that creates a simple transaction boundary around the state change.</p><p>It is deliberately not a distributed-lock solution.</p><p>If a remote service controlled thousands of physical machines, the concurrency boundary would need to move into durable storage and device-command handling.</p><h2>There Is One Simplification You Should Mention in the Interview</h2><p>Our <code>DispenseResult</code> returns change as:</p><pre><code>int changeInPaise</code></pre><p>Real vending hardware cannot manufacture arbitrary change.</p><p>It has a finite number of particular coins or notes.</p><p>So a more advanced version needs something like:</p><pre><code>public interface ChangeService { List<Coin> makeChange(int amountInPaise);}</code></pre><p>The implementation would track available denominations and determine whether exact change can be produced.</p><p>That creates another important state:</p><pre><code>EXACT_CHANGE_ONLY</code></pre><p>or a rule preventing purchases the machine cannot settle.</p><p>I would <strong>mention this extension rather than implementing it immediately</strong> unless the interviewer explicitly asks for denomination-level cash management.</p><p>That keeps the first solution focused.</p><h2>Where Candidates Usually Overcomplicate This Problem</h2><p>The first trap is creating subclasses for everything:</p><pre><code>CokeProductPepsiProductWaterProduct</code></pre><p>If their only difference is name and price, those are data values, not separate behaviours.</p><p>The second is adding:</p><pre><code>ProductFactoryCoinFactoryInventoryFactoryVendingMachineSingleton</code></pre><p>without any construction problem that requires them.</p><p>The third is declaring the State Pattern before identifying any actual states.</p><p>That reverses the reasoning.</p><p>We did not decide:</p><blockquote><p>“This is a famous State Pattern interview question, so let's use State.”</p></blockquote><p>We observed:</p><blockquote><p>“The same operation has different legal behaviour depending on previous events.”</p></blockquote><p><strong>That</strong> is the reason State fits.</p><h2>What Happens When Requirements Change?</h2><p>Suppose the interviewer says:</p><p><strong>“Let customers select first and pay afterward.”</strong></p><p>Now introduce something like:</p><pre><code>IDLE ↓ selectionPRODUCT_SELECTED ↓ sufficient moneyDISPENSING</code></pre><p>The existing states do not need enormous conditional rewrites.</p><hr><p><strong>“The customer can pay by card.”</strong></p><p>Separate payment authorization from physical cash credit.</p><p>The transaction state machine can still remain.</p><hr><p><strong>“A dispensing motor can fail after payment.”</strong></p><p>Now the happy path is no longer atomic.</p><p>You need to model:</p><pre><code>payment accepted ↓dispense attempted ↓success → finishfailure → refund / operator intervention</code></pre><p>That is where a <code>DispenseFailedState</code> could become legitimate.</p><hr><p><strong>“The operator needs to restock the machine.”</strong></p><p>That is a different actor and probably a different administrative interface. It should not be forced into the customer's transaction state machine.</p><p>Good LLD evolves by introducing concepts when the requirements make them necessary.</p><h2>How I Would Handle This in an Interview</h2><p>For this problem, I would spend less time drawing a giant class diagram and more time showing the state transitions early.</p><p>The conversation should roughly progress like this:</p><p><strong>First</strong>, clarify whether payment happens before selection, whether change is required, and whether exact denomination handling is expected.</p><p><strong>Then</strong>, identify the transaction states.</p><p>Once the interviewer agrees with:</p><pre><code>IDLE → HAS_CREDIT → DISPENSING → IDLE</code></pre><p>the rest becomes much easier.</p><p>Next introduce <code>Product</code>, <code>ProductSlot</code>, and <code>Inventory</code>, followed by the <code>VendingState</code> interface.</p><p>Code one complete successful purchase and one invalid transition.</p><p>Only after that discuss concurrency, exact-change handling, card payments, or hardware failures.</p><p>That tells the interviewer you can distinguish <strong>the core model from the extensions</strong>.</p><h2>The Lesson From LLD 02</h2><p>Parking Lot taught us to ask:</p><blockquote><p>Which object should own this responsibility?</p></blockquote><p>Vending Machine adds another question:</p><blockquote><p>Which actions are valid <strong>right now</strong>?</p></blockquote><p>That question appears everywhere in backend software.</p><p>An order might move through:</p><pre><code>CREATED → PAID → SHIPPED → DELIVERED</code></pre><p>A payment might move through:</p><pre><code>INITIATED → AUTHORIZED → CAPTURED</code></pre><p>A job might move through:</p><pre><code>QUEUED → RUNNING → SUCCEEDED</code></pre><p>Whenever behaviour depends strongly on lifecycle state, scattering state checks around the code becomes increasingly dangerous.</p><p>The State Pattern is not valuable because it gives us more classes.</p><p>It is valuable because it puts the rules for each state in one place and makes invalid transitions difficult to ignore.</p><p>That is the real design lesson hidden inside a vending machine.</p><h2>Official References</h2><ul><li><p><a href="https://docs.oracle.com/javase/tutorial/java/javaOO/enum.html" target="_blank" rel="noopener noreferrer ugc">Oracle Java — Enum Types</a></p></li><li><p><a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/sync.html" target="_blank" rel="noopener noreferrer ugc">Oracle Java — Synchronization</a></p></li><li><p><a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/syncmeth.html" target="_blank" rel="noopener noreferrer ugc">Oracle Java — Synchronized Methods</a></p></li></ul>
Conversation
Comments