Designing a Vending Machine
Difficulty: Beginner Patterns: State, Strategy, Singleton (inventory) Asked at: PhonePe, Amazon, Flipkart, Uber, Walmart
The vending machine is the textbook problem for the State pattern. The whole point interviewers are testing: can you model a machine whose behaviour for the same action (say, “insert coin”) changes depending on what state it’s in — without a tangle of if (state == ...) checks scattered through every method?
Functional Requirements
- Machine holds multiple products, each in a slot with a price and a quantity.
- User flow: select a product → insert money → collect product + change.
- Accept money in fixed denominations (coins and notes).
- Return change when the user overpays, using available denominations.
- Allow the user to cancel and get a full refund at any point before dispensing.
- Reject selection when the product is out of stock or the machine can’t make change.
- Operator can refill inventory and restock the coin float.
Non-Functional Requirements
- No invalid transitions — you can’t dispense before paying, can’t pay before selecting.
- Never over-dispense — money and inventory stay consistent even on cancel.
- Extensibility — adding a new state (e.g. maintenance mode) or a new change algorithm shouldn’t touch existing states.
Core Entities
| Entity | Description |
|---|---|
Coin / Note |
Enum of accepted denominations (value in cents/paise) |
Product |
Name + price |
Slot |
Holds a product, its price, and remaining quantity |
Inventory<T> |
Generic count-map: products in slots, coins in the float |
VendingMachineState |
Interface — one method per possible user action |
IdleState |
Waiting for a product selection |
HasSelectionState |
Product chosen, waiting for money |
HasMoneyState |
Enough money in; ready to dispense |
DispenseState |
Hands over product + change, resets to idle |
ChangeStrategy |
How to compute change from available denominations |
VendingMachine |
Context — holds current state, inventory, and the money pool |
The State Pattern — why it fits
💡 State pattern = let an object alter its behaviour when its internal state changes. The object appears to change its class. Each state is a class; the context delegates every action to its current state object.
A naive vending machine has one giant insertCoin() method full of if (currentState == IDLE) ... else if (currentState == HAS_MONEY) .... Every action method repeats that ladder, and adding a state means editing all of them. The State pattern flips it: each state is a class that knows how to handle every action for that state, including rejecting the ones that don’t make sense. The machine just forwards calls to currentState.
stateDiagram-v2
[*] --> Idle
Idle --> HasSelection : selectProduct()
HasSelection --> HasSelection : insertMoney() (not enough yet)
HasSelection --> HasMoney : insertMoney() (enough)
HasMoney --> Dispense : dispense()
Dispense --> Idle : product + change returned
HasSelection --> Idle : cancel() (refund)
HasMoney --> Idle : cancel() (refund)
Class Diagram
classDiagram
class VendingMachineState {
<<interface>>
+selectProduct(String code)
+insertMoney(int amount)
+dispense()
+cancel()
}
class VendingMachine {
-VendingMachineState idle
-VendingMachineState hasSelection
-VendingMachineState hasMoney
-VendingMachineState dispense
-VendingMachineState current
-Inventory~String~ products
-Inventory~Integer~ coins
-Slot selectedSlot
-int balance
-ChangeStrategy changeStrategy
+selectProduct(String)
+insertMoney(int)
+dispense()
+cancel()
+setState(VendingMachineState)
}
class Slot {
-Product product
-int quantity
+dispenseOne()
}
class ChangeStrategy {
<<interface>>
+makeChange(int amount, Inventory~Integer~ float) List~Integer~
}
class GreedyChangeStrategy {
+makeChange(int, Inventory) List~Integer~
}
VendingMachineState <|.. IdleState
VendingMachineState <|.. HasSelectionState
VendingMachineState <|.. HasMoneyState
VendingMachineState <|.. DispenseState
VendingMachine --> VendingMachineState : current
VendingMachine --> Slot
VendingMachine --> ChangeStrategy
ChangeStrategy <|.. GreedyChangeStrategy
Design Patterns
| Pattern | Where | Why |
|---|---|---|
| State | VendingMachineState with four concrete states |
Behaviour of each action depends on the state; no if/else state ladders. New state = one class. |
| Strategy | ChangeStrategy (greedy today, DP-optimal tomorrow) |
Swap the change algorithm without touching the machine. |
| Context | VendingMachine holds state + shared data |
States are stateless singletons; all mutable data lives in one place. |
Data Structures
| Component | Structure | Why |
|---|---|---|
| Product slots | Map<String, Slot> keyed by code (e.g. "A1") |
O(1) selection by keypad code |
| Coin float | Map<Integer, Integer> (denomination → count) |
O(1) update; greedy change walks denominations high→low |
| State objects | One instance each, reused | States hold no per-transaction data, so they’re safe to share |
Complete Code
Product.java
An immutable value object — just a name and a price (in the smallest currency unit, e.g. paise, to avoid floating-point money bugs).
package vending.model;
public class Product {
private final String name;
private final int price; // in paise/cents — never use double for money
public Product(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() { return name; }
public int getPrice() { return price; }
@Override
public String toString() { return name + " (₹" + price / 100.0 + ")"; }
}
class Product:
"""Immutable value object — name and price in paise/cents."""
def __init__(self, name: str, price: int) -> None:
self._name = name
self._price = price # in paise/cents — never use float for money
@property
def name(self) -> str:
return self._name
@property
def price(self) -> int:
return self._price
def __str__(self) -> str:
return f"{self._name} (₹{self._price / 100.0})"
#pragma once
#include <string>
#include <sstream>
class Product {
std::string name_;
int price_; // in paise/cents — never use double for money
public:
Product(std::string name, int price)
: name_(std::move(name)), price_(price) {}
const std::string& getName() const { return name_; }
int getPrice() const { return price_; }
std::string toString() const {
std::ostringstream oss;
oss << name_ << " (₹" << price_ / 100.0 << ")";
return oss.str();
}
};
class Product {
#name;
#price; // in paise/cents — never use float for money
constructor(name, price) {
this.#name = name;
this.#price = price;
}
get name() { return this.#name; }
get price() { return this.#price; }
toString() {
return `${this.#name} (₹${this.#price / 100.0})`;
}
}
Slot.java
A slot pairs a product with its remaining quantity. dispenseOne() is the only mutator, and it guards against dispensing from an empty slot — the machine should never hand out what it doesn’t have.
package vending.model;
public class Slot {
private final Product product;
private int quantity;
public Slot(Product product, int quantity) {
this.product = product;
this.quantity = quantity;
}
public Product getProduct() { return product; }
public int getQuantity() { return quantity; }
public boolean isAvailable() { return quantity > 0; }
public void refill(int count) { quantity += count; }
public void dispenseOne() {
if (quantity <= 0) throw new IllegalStateException("Slot empty: " + product.getName());
quantity--;
}
}
class Slot:
"""Pairs a product with its remaining quantity."""
def __init__(self, product: "Product", quantity: int) -> None:
self._product = product
self._quantity = quantity
@property
def product(self) -> "Product":
return self._product
@property
def quantity(self) -> int:
return self._quantity
def is_available(self) -> bool:
return self._quantity > 0
def refill(self, count: int) -> None:
self._quantity += count
def dispense_one(self) -> None:
if self._quantity <= 0:
raise RuntimeError(f"Slot empty: {self._product.name}")
self._quantity -= 1
#pragma once
#include <stdexcept>
#include "Product.hpp"
class Slot {
Product product_;
int quantity_;
public:
Slot(Product product, int quantity)
: product_(std::move(product)), quantity_(quantity) {}
const Product& getProduct() const { return product_; }
int getQuantity() const { return quantity_; }
bool isAvailable() const { return quantity_ > 0; }
void refill(int count) { quantity_ += count; }
void dispenseOne() {
if (quantity_ <= 0)
throw std::runtime_error("Slot empty: " + product_.getName());
quantity_--;
}
};
class Slot {
#product;
#quantity;
constructor(product, quantity) {
this.#product = product;
this.#quantity = quantity;
}
get product() { return this.#product; }
get quantity() { return this.#quantity; }
isAvailable() { return this.#quantity > 0; }
refill(count) { this.#quantity += count; }
dispenseOne() {
if (this.#quantity <= 0)
throw new Error(`Slot empty: ${this.#product.name}`);
this.#quantity--;
}
}
ChangeStrategy.java (Strategy interface)
Computing change is a swappable algorithm. The greedy version is fine when denominations are canonical (1, 2, 5, 10…); a DP version would be needed for exotic denomination sets. Isolating it behind an interface means the machine doesn’t care which one runs.
package vending.change;
import java.util.List;
import java.util.Map;
public interface ChangeStrategy {
/**
* @param amount the change owed, in paise
* @param floatCoins available denominations (value → count), mutated on success
* @return the list of denominations to return
* @throws CannotMakeChangeException if exact change isn't possible
*/
List<Integer> makeChange(int amount, Map<Integer, Integer> floatCoins);
}
from abc import ABC, abstractmethod
class ChangeStrategy(ABC):
"""Interface for computing change from available denominations."""
@abstractmethod
def make_change(self, amount: int, float_coins: dict[int, int]) -> list[int]:
"""
Compute change.
Args:
amount: the change owed, in paise
float_coins: available denominations (value → count), mutated on success
Returns:
the list of denominations to return
Raises:
CannotMakeChangeError: if exact change isn't possible
"""
...
#pragma once
#include <vector>
#include <unordered_map>
class ChangeStrategy {
public:
virtual ~ChangeStrategy() = default;
/**
* @param amount the change owed, in paise
* @param floatCoins available denominations (value → count), mutated on success
* @return the list of denominations to return
* @throws CannotMakeChangeException if exact change isn't possible
*/
virtual std::vector<int> makeChange(
int amount, std::unordered_map<int, int>& floatCoins) = 0;
};
/**
* Interface for computing change from available denominations.
* Subclasses must implement makeChange(amount, floatCoins).
*/
class ChangeStrategy {
/**
* @param {number} amount - the change owed, in paise
* @param {Map<number,number>} floatCoins - denomination → count, mutated on success
* @returns {number[]} the list of denominations to return
* @throws {CannotMakeChangeError} if exact change isn't possible
*/
makeChange(amount, floatCoins) {
throw new Error("makeChange() must be implemented by subclass");
}
}
CannotMakeChangeException.java
package vending.change;
public class CannotMakeChangeException extends RuntimeException {
public CannotMakeChangeException(String message) { super(message); }
}
class CannotMakeChangeError(RuntimeError):
"""Raised when exact change cannot be made from available denominations."""
def __init__(self, message: str) -> None:
super().__init__(message)
#pragma once
#include <stdexcept>
#include <string>
class CannotMakeChangeException : public std::runtime_error {
public:
explicit CannotMakeChangeException(const std::string& message)
: std::runtime_error(message) {}
};
class CannotMakeChangeError extends Error {
constructor(message) {
super(message);
this.name = "CannotMakeChangeError";
}
}
GreedyChangeStrategy.java
Walks denominations from largest to smallest, taking as many of each as it can. It only commits to the coin float once it knows exact change is possible — so a failed attempt leaves the float untouched.
package vending.change;
import java.util.*;
public class GreedyChangeStrategy implements ChangeStrategy {
@Override
public List<Integer> makeChange(int amount, Map<Integer, Integer> floatCoins) {
List<Integer> denoms = new ArrayList<>(floatCoins.keySet());
denoms.sort(Collections.reverseOrder());
List<Integer> result = new ArrayList<>();
Map<Integer, Integer> used = new HashMap<>();
int remaining = amount;
for (int d : denoms) {
int available = floatCoins.getOrDefault(d, 0);
int take = Math.min(remaining / d, available);
for (int i = 0; i < take; i++) result.add(d);
if (take > 0) used.put(d, take);
remaining -= take * d;
}
if (remaining != 0) {
throw new CannotMakeChangeException("Cannot make exact change for " + amount);
}
// Commit only now that we know it worked.
used.forEach((d, count) -> floatCoins.merge(d, -count, Integer::sum));
return result;
}
}
class GreedyChangeStrategy(ChangeStrategy):
"""Greedy: walks denominations high→low, takes as many as possible."""
def make_change(self, amount: int, float_coins: dict[int, int]) -> list[int]:
denoms = sorted(float_coins.keys(), reverse=True)
result: list[int] = []
used: dict[int, int] = {}
remaining = amount
for d in denoms:
available = float_coins.get(d, 0)
take = min(remaining // d, available)
result.extend([d] * take)
if take > 0:
used[d] = take
remaining -= take * d
if remaining != 0:
raise CannotMakeChangeError(f"Cannot make exact change for {amount}")
# Commit only now that we know it worked.
for d, count in used.items():
float_coins[d] -= count
return result
#pragma once
#include <vector>
#include <algorithm>
#include <unordered_map>
#include "ChangeStrategy.hpp"
#include "CannotMakeChangeException.hpp"
class GreedyChangeStrategy : public ChangeStrategy {
public:
std::vector<int> makeChange(
int amount, std::unordered_map<int, int>& floatCoins) override {
std::vector<int> denoms;
for (auto& [d, _] : floatCoins) denoms.push_back(d);
std::sort(denoms.begin(), denoms.end(), std::greater<>());
std::vector<int> result;
std::unordered_map<int, int> used;
int remaining = amount;
for (int d : denoms) {
int available = floatCoins.count(d) ? floatCoins[d] : 0;
int take = std::min(remaining / d, available);
for (int i = 0; i < take; ++i) result.push_back(d);
if (take > 0) used[d] = take;
remaining -= take * d;
}
if (remaining != 0) {
throw CannotMakeChangeException(
"Cannot make exact change for " + std::to_string(amount));
}
// Commit only now that we know it worked.
for (auto& [d, count] : used) floatCoins[d] -= count;
return result;
}
};
class GreedyChangeStrategy extends ChangeStrategy {
makeChange(amount, floatCoins) {
const denoms = [...floatCoins.keys()].sort((a, b) => b - a);
const result = [];
const used = new Map();
let remaining = amount;
for (const d of denoms) {
const available = floatCoins.get(d) || 0;
const take = Math.min(Math.floor(remaining / d), available);
for (let i = 0; i < take; i++) result.push(d);
if (take > 0) used.set(d, take);
remaining -= take * d;
}
if (remaining !== 0) {
throw new CannotMakeChangeError(`Cannot make exact change for ${amount}`);
}
// Commit only now that we know it worked.
for (const [d, count] of used) {
floatCoins.set(d, floatCoins.get(d) - count);
}
return result;
}
}
VendingMachineState.java (State interface)
One method per user action. Every concrete state must decide how to handle each — including the ones it should reject.
package vending.state;
public interface VendingMachineState {
void selectProduct(String code);
void insertMoney(int amount);
void dispense();
void cancel();
String name();
}
from abc import ABC, abstractmethod
class VendingMachineState(ABC):
"""One method per user action."""
@abstractmethod
def select_product(self, code: str) -> None: ...
@abstractmethod
def insert_money(self, amount: int) -> None: ...
@abstractmethod
def dispense(self) -> None: ...
@abstractmethod
def cancel(self) -> None: ...
@abstractmethod
def name(self) -> str: ...
#pragma once
#include <string>
class VendingMachineState {
public:
virtual ~VendingMachineState() = default;
virtual void selectProduct(const std::string& code) = 0;
virtual void insertMoney(int amount) = 0;
virtual void dispense() = 0;
virtual void cancel() = 0;
virtual std::string name() const = 0;
};
class VendingMachineState {
selectProduct(code) { throw new Error("Not implemented"); }
insertMoney(amount) { throw new Error("Not implemented"); }
dispense() { throw new Error("Not implemented"); }
cancel() { throw new Error("Not implemented"); }
get stateName() { throw new Error("Not implemented"); }
}
IdleState.java
The starting state. Only selectProduct does anything meaningful; inserting money or trying to dispense here is a user error and is rejected with a clear message.
package vending.state;
import vending.VendingMachine;
import vending.model.Slot;
public class IdleState implements VendingMachineState {
private final VendingMachine machine;
public IdleState(VendingMachine machine) { this.machine = machine; }
@Override
public void selectProduct(String code) {
Slot slot = machine.getSlot(code);
if (slot == null) { System.out.println("✗ No such product: " + code); return; }
if (!slot.isAvailable()) { System.out.println("✗ Out of stock: " + slot.getProduct().getName()); return; }
machine.setSelectedSlot(slot);
System.out.println("→ Selected " + slot.getProduct() + ". Please insert money.");
machine.setState(machine.getHasSelectionState());
}
@Override public void insertMoney(int amount) { System.out.println("✗ Select a product first."); }
@Override public void dispense() { System.out.println("✗ Select a product first."); }
@Override public void cancel() { System.out.println("Nothing to cancel."); }
@Override public String name() { return "IDLE"; }
}
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from vending_machine import VendingMachine
class IdleState(VendingMachineState):
"""Waiting for a product selection."""
def __init__(self, machine: "VendingMachine") -> None:
self._machine = machine
def select_product(self, code: str) -> None:
slot = self._machine.get_slot(code)
if slot is None:
print(f"✗ No such product: {code}")
return
if not slot.is_available():
print(f"✗ Out of stock: {slot.product.name}")
return
self._machine.selected_slot = slot
print(f"→ Selected {slot.product}. Please insert money.")
self._machine.set_state(self._machine.has_selection_state)
def insert_money(self, amount: int) -> None:
print("✗ Select a product first.")
def dispense(self) -> None:
print("✗ Select a product first.")
def cancel(self) -> None:
print("Nothing to cancel.")
def name(self) -> str:
return "IDLE"
#pragma once
#include <iostream>
#include "VendingMachineState.hpp"
class VendingMachine; // forward declaration
class IdleState : public VendingMachineState {
VendingMachine& machine_;
public:
explicit IdleState(VendingMachine& machine) : machine_(machine) {}
void selectProduct(const std::string& code) override {
auto* slot = machine_.getSlot(code);
if (!slot) { std::cout << "✗ No such product: " << code << "\n"; return; }
if (!slot->isAvailable()) {
std::cout << "✗ Out of stock: " << slot->getProduct().getName() << "\n";
return;
}
machine_.setSelectedSlot(slot);
std::cout << "→ Selected " << slot->getProduct().toString()
<< ". Please insert money.\n";
machine_.setState(machine_.getHasSelectionState());
}
void insertMoney(int) override { std::cout << "✗ Select a product first.\n"; }
void dispense() override { std::cout << "✗ Select a product first.\n"; }
void cancel() override { std::cout << "Nothing to cancel.\n"; }
std::string name() const override { return "IDLE"; }
};
class IdleState extends VendingMachineState {
#machine;
constructor(machine) {
super();
this.#machine = machine;
}
selectProduct(code) {
const slot = this.#machine.getSlot(code);
if (!slot) { console.log(`✗ No such product: ${code}`); return; }
if (!slot.isAvailable()) {
console.log(`✗ Out of stock: ${slot.product.name}`);
return;
}
this.#machine.selectedSlot = slot;
console.log(`→ Selected ${slot.product}. Please insert money.`);
this.#machine.setState(this.#machine.hasSelectionState);
}
insertMoney(amount) { console.log("✗ Select a product first."); }
dispense() { console.log("✗ Select a product first."); }
cancel() { console.log("Nothing to cancel."); }
get stateName() { return "IDLE"; }
}
HasSelectionState.java
A product is chosen. Money accumulates here; once the balance reaches the price, the machine advances to HasMoney. Note that selecting again just replaces the choice, and cancel refunds whatever’s been inserted.
package vending.state;
import vending.VendingMachine;
public class HasSelectionState implements VendingMachineState {
private final VendingMachine machine;
public HasSelectionState(VendingMachine machine) { this.machine = machine; }
@Override
public void selectProduct(String code) {
// Allow re-selecting while no money is in; delegate to idle logic.
machine.setState(machine.getIdleState());
machine.selectProduct(code);
}
@Override
public void insertMoney(int amount) {
machine.addToBalance(amount);
int price = machine.getSelectedSlot().getProduct().getPrice();
System.out.println(" Balance: ₹" + machine.getBalance() / 100.0 + " / ₹" + price / 100.0);
if (machine.getBalance() >= price) {
System.out.println("→ Enough money in. Press dispense.");
machine.setState(machine.getHasMoneyState());
}
}
@Override public void dispense() { System.out.println("✗ Insert more money first."); }
@Override
public void cancel() {
machine.refundBalance();
machine.reset();
machine.setState(machine.getIdleState());
}
@Override public String name() { return "HAS_SELECTION"; }
}
class HasSelectionState(VendingMachineState):
"""Product chosen, waiting for money."""
def __init__(self, machine: "VendingMachine") -> None:
self._machine = machine
def select_product(self, code: str) -> None:
# Allow re-selecting; delegate to idle logic.
self._machine.set_state(self._machine.idle_state)
self._machine.select_product(code)
def insert_money(self, amount: int) -> None:
self._machine.add_to_balance(amount)
price = self._machine.selected_slot.product.price
print(f" Balance: ₹{self._machine.balance / 100.0} / ₹{price / 100.0}")
if self._machine.balance >= price:
print("→ Enough money in. Press dispense.")
self._machine.set_state(self._machine.has_money_state)
def dispense(self) -> None:
print("✗ Insert more money first.")
def cancel(self) -> None:
self._machine.refund_balance()
self._machine.reset()
self._machine.set_state(self._machine.idle_state)
def name(self) -> str:
return "HAS_SELECTION"
#pragma once
#include <iostream>
#include "VendingMachineState.hpp"
class VendingMachine;
class HasSelectionState : public VendingMachineState {
VendingMachine& machine_;
public:
explicit HasSelectionState(VendingMachine& machine) : machine_(machine) {}
void selectProduct(const std::string& code) override {
// Allow re-selecting; delegate to idle logic.
machine_.setState(machine_.getIdleState());
machine_.selectProduct(code);
}
void insertMoney(int amount) override {
machine_.addToBalance(amount);
int price = machine_.getSelectedSlot()->getProduct().getPrice();
std::cout << " Balance: ₹" << machine_.getBalance() / 100.0
<< " / ₹" << price / 100.0 << "\n";
if (machine_.getBalance() >= price) {
std::cout << "→ Enough money in. Press dispense.\n";
machine_.setState(machine_.getHasMoneyState());
}
}
void dispense() override { std::cout << "✗ Insert more money first.\n"; }
void cancel() override {
machine_.refundBalance();
machine_.reset();
machine_.setState(machine_.getIdleState());
}
std::string name() const override { return "HAS_SELECTION"; }
};
class HasSelectionState extends VendingMachineState {
#machine;
constructor(machine) {
super();
this.#machine = machine;
}
selectProduct(code) {
// Allow re-selecting; delegate to idle logic.
this.#machine.setState(this.#machine.idleState);
this.#machine.selectProduct(code);
}
insertMoney(amount) {
this.#machine.addToBalance(amount);
const price = this.#machine.selectedSlot.product.price;
console.log(` Balance: ₹${this.#machine.balance / 100.0} / ₹${price / 100.0}`);
if (this.#machine.balance >= price) {
console.log("→ Enough money in. Press dispense.");
this.#machine.setState(this.#machine.hasMoneyState);
}
}
dispense() { console.log("✗ Insert more money first."); }
cancel() {
this.#machine.refundBalance();
this.#machine.reset();
this.#machine.setState(this.#machine.idleState);
}
get stateName() { return "HAS_SELECTION"; }
}
HasMoneyState.java
Enough money is in. dispense is now valid: it tries to compute change before committing anything, drops the product, returns the change, and resets. If change can’t be made, the whole transaction is refunded — the machine never keeps money it can’t complete a sale for.
package vending.state;
import vending.VendingMachine;
import vending.change.CannotMakeChangeException;
import vending.model.Slot;
import java.util.List;
public class HasMoneyState implements VendingMachineState {
private final VendingMachine machine;
public HasMoneyState(VendingMachine machine) { this.machine = machine; }
@Override public void selectProduct(String code) { System.out.println("✗ Finish or cancel the current purchase first."); }
@Override public void insertMoney(int amount) { machine.addToBalance(amount); System.out.println(" Extra money accepted; will be returned as change."); }
@Override
public void dispense() {
machine.setState(machine.getDispenseState());
machine.dispense(); // delegate the actual work to the dispense state
}
@Override
public void cancel() {
machine.refundBalance();
machine.reset();
machine.setState(machine.getIdleState());
}
@Override public String name() { return "HAS_MONEY"; }
}
class HasMoneyState(VendingMachineState):
"""Enough money inserted; ready to dispense."""
def __init__(self, machine: "VendingMachine") -> None:
self._machine = machine
def select_product(self, code: str) -> None:
print("✗ Finish or cancel the current purchase first.")
def insert_money(self, amount: int) -> None:
self._machine.add_to_balance(amount)
print(" Extra money accepted; will be returned as change.")
def dispense(self) -> None:
self._machine.set_state(self._machine.dispense_state)
self._machine.dispense() # delegate actual work to the dispense state
def cancel(self) -> None:
self._machine.refund_balance()
self._machine.reset()
self._machine.set_state(self._machine.idle_state)
def name(self) -> str:
return "HAS_MONEY"
#pragma once
#include <iostream>
#include "VendingMachineState.hpp"
class VendingMachine;
class HasMoneyState : public VendingMachineState {
VendingMachine& machine_;
public:
explicit HasMoneyState(VendingMachine& machine) : machine_(machine) {}
void selectProduct(const std::string&) override {
std::cout << "✗ Finish or cancel the current purchase first.\n";
}
void insertMoney(int amount) override {
machine_.addToBalance(amount);
std::cout << " Extra money accepted; will be returned as change.\n";
}
void dispense() override {
machine_.setState(machine_.getDispenseState());
machine_.dispense(); // delegate actual work to the dispense state
}
void cancel() override {
machine_.refundBalance();
machine_.reset();
machine_.setState(machine_.getIdleState());
}
std::string name() const override { return "HAS_MONEY"; }
};
class HasMoneyState extends VendingMachineState {
#machine;
constructor(machine) {
super();
this.#machine = machine;
}
selectProduct(code) { console.log("✗ Finish or cancel the current purchase first."); }
insertMoney(amount) {
this.#machine.addToBalance(amount);
console.log(" Extra money accepted; will be returned as change.");
}
dispense() {
this.#machine.setState(this.#machine.dispenseState);
this.#machine.dispense(); // delegate actual work to the dispense state
}
cancel() {
this.#machine.refundBalance();
this.#machine.reset();
this.#machine.setState(this.#machine.idleState);
}
get stateName() { return "HAS_MONEY"; }
}
DispenseState.java
The transactional core. Order matters: compute change first (this can fail), and only then drop the product and bank the money. On failure, refund and abort — leaving inventory and float exactly as they were.
package vending.state;
import vending.VendingMachine;
import vending.change.CannotMakeChangeException;
import vending.model.Slot;
import java.util.List;
public class DispenseState implements VendingMachineState {
private final VendingMachine machine;
public DispenseState(VendingMachine machine) { this.machine = machine; }
@Override public void selectProduct(String code) { System.out.println("✗ Dispensing, please wait."); }
@Override public void insertMoney(int amount) { System.out.println("✗ Dispensing, please wait."); }
@Override
public void dispense() {
Slot slot = machine.getSelectedSlot();
int price = slot.getProduct().getPrice();
int changeOwed = machine.getBalance() - price;
try {
// 1. Work out change BEFORE touching stock or money.
List<Integer> change = machine.getChangeStrategy()
.makeChange(changeOwed, machine.getCoinFloat());
// 2. Commit: drop product, bank the money paid.
slot.dispenseOne();
machine.bankBalance(); // the inserted coins now belong to the float
System.out.println("✓ Dispensed: " + slot.getProduct().getName());
if (!change.isEmpty()) System.out.println("✓ Change returned: " + change);
} catch (CannotMakeChangeException e) {
System.out.println("✗ " + e.getMessage() + " — refunding.");
machine.refundBalance();
} finally {
machine.reset();
machine.setState(machine.getIdleState());
}
}
@Override public void cancel() { System.out.println("✗ Too late to cancel."); }
@Override public String name() { return "DISPENSE"; }
}
class DispenseState(VendingMachineState):
"""Transactional core: compute change, then drop product."""
def __init__(self, machine: "VendingMachine") -> None:
self._machine = machine
def select_product(self, code: str) -> None:
print("✗ Dispensing, please wait.")
def insert_money(self, amount: int) -> None:
print("✗ Dispensing, please wait.")
def dispense(self) -> None:
slot = self._machine.selected_slot
price = slot.product.price
change_owed = self._machine.balance - price
try:
# 1. Work out change BEFORE touching stock or money.
change = self._machine.change_strategy.make_change(
change_owed, self._machine.coin_float
)
# 2. Commit: drop product, bank the money paid.
slot.dispense_one()
self._machine.bank_balance()
print(f"✓ Dispensed: {slot.product.name}")
if change:
print(f"✓ Change returned: {change}")
except CannotMakeChangeError as e:
print(f"✗ {e} — refunding.")
self._machine.refund_balance()
finally:
self._machine.reset()
self._machine.set_state(self._machine.idle_state)
def cancel(self) -> None:
print("✗ Too late to cancel.")
def name(self) -> str:
return "DISPENSE"
#pragma once
#include <iostream>
#include <vector>
#include "VendingMachineState.hpp"
#include "CannotMakeChangeException.hpp"
class VendingMachine;
class DispenseState : public VendingMachineState {
VendingMachine& machine_;
public:
explicit DispenseState(VendingMachine& machine) : machine_(machine) {}
void selectProduct(const std::string&) override {
std::cout << "✗ Dispensing, please wait.\n";
}
void insertMoney(int) override {
std::cout << "✗ Dispensing, please wait.\n";
}
void dispense() override {
auto* slot = machine_.getSelectedSlot();
int price = slot->getProduct().getPrice();
int changeOwed = machine_.getBalance() - price;
try {
// 1. Work out change BEFORE touching stock or money.
auto change = machine_.getChangeStrategy()->makeChange(
changeOwed, machine_.getCoinFloat());
// 2. Commit: drop product, bank the money paid.
slot->dispenseOne();
machine_.bankBalance();
std::cout << "✓ Dispensed: " << slot->getProduct().getName() << "\n";
if (!change.empty()) {
std::cout << "✓ Change returned: [";
for (size_t i = 0; i < change.size(); ++i) {
if (i > 0) std::cout << ", ";
std::cout << change[i];
}
std::cout << "]\n";
}
} catch (const CannotMakeChangeException& e) {
std::cout << "✗ " << e.what() << " — refunding.\n";
machine_.refundBalance();
}
machine_.reset();
machine_.setState(machine_.getIdleState());
}
void cancel() override { std::cout << "✗ Too late to cancel.\n"; }
std::string name() const override { return "DISPENSE"; }
};
class DispenseState extends VendingMachineState {
#machine;
constructor(machine) {
super();
this.#machine = machine;
}
selectProduct(code) { console.log("✗ Dispensing, please wait."); }
insertMoney(amount) { console.log("✗ Dispensing, please wait."); }
dispense() {
const slot = this.#machine.selectedSlot;
const price = slot.product.price;
const changeOwed = this.#machine.balance - price;
try {
// 1. Work out change BEFORE touching stock or money.
const change = this.#machine.changeStrategy
.makeChange(changeOwed, this.#machine.coinFloat);
// 2. Commit: drop product, bank the money paid.
slot.dispenseOne();
this.#machine.bankBalance();
console.log(`✓ Dispensed: ${slot.product.name}`);
if (change.length > 0) console.log(`✓ Change returned: [${change}]`);
} catch (e) {
if (e instanceof CannotMakeChangeError) {
console.log(`✗ ${e.message} — refunding.`);
this.#machine.refundBalance();
} else throw e;
} finally {
this.#machine.reset();
this.#machine.setState(this.#machine.idleState);
}
}
cancel() { console.log("✗ Too late to cancel."); }
get stateName() { return "DISPENSE"; }
}
VendingMachine.java (Context)
The context owns the four state singletons, the inventory, the coin float, and the in-flight transaction data (selectedSlot, balance). Every public action just forwards to current — the machine itself contains no branching on state.
package vending;
import vending.change.ChangeStrategy;
import vending.change.GreedyChangeStrategy;
import vending.model.Product;
import vending.model.Slot;
import vending.state.*;
import java.util.*;
public class VendingMachine {
// States (created once, reused)
private final VendingMachineState idle = new IdleState(this);
private final VendingMachineState hasSelection = new HasSelectionState(this);
private final VendingMachineState hasMoney = new HasMoneyState(this);
private final VendingMachineState dispenseState = new DispenseState(this);
private VendingMachineState current = idle;
// Data
private final Map<String, Slot> slots = new HashMap<>();
private final Map<Integer, Integer> coinFloat = new HashMap<>(); // denomination → count
private ChangeStrategy changeStrategy = new GreedyChangeStrategy();
// In-flight transaction
private Slot selectedSlot;
private int balance; // money inserted this transaction (paise)
private final List<Integer> insertedCoins = new ArrayList<>();
// ─── Public actions: delegate to current state ───────────
public void selectProduct(String code) { current.selectProduct(code); }
public void dispense() { current.dispense(); }
public void cancel() { current.cancel(); }
public void insertMoney(int denomination) {
coinFloatCandidate(denomination); // remember it so we can refund exact coins
current.insertMoney(denomination);
}
// ─── State accessors (used by states) ────────────────────
public VendingMachineState getIdleState() { return idle; }
public VendingMachineState getHasSelectionState() { return hasSelection; }
public VendingMachineState getHasMoneyState() { return hasMoney; }
public VendingMachineState getDispenseState() { return dispenseState; }
public void setState(VendingMachineState s) { this.current = s; }
public String currentStateName() { return current.name(); }
// ─── Transaction data (used by states) ───────────────────
public Slot getSlot(String code) { return slots.get(code); }
public Slot getSelectedSlot() { return selectedSlot; }
public void setSelectedSlot(Slot s) { this.selectedSlot = s; }
public int getBalance() { return balance; }
public void addToBalance(int amount) { balance += amount; }
public ChangeStrategy getChangeStrategy() { return changeStrategy; }
public void setChangeStrategy(ChangeStrategy s) { this.changeStrategy = s; }
public Map<Integer, Integer> getCoinFloat() { return coinFloat; }
private void coinFloatCandidate(int denomination) { insertedCoins.add(denomination); }
/** The coins inserted this transaction become part of the float (sale committed). */
public void bankBalance() {
for (int c : insertedCoins) coinFloat.merge(c, 1, Integer::sum);
}
/** Give the inserted coins back to the user (cancel / cannot-make-change). */
public void refundBalance() {
if (!insertedCoins.isEmpty()) System.out.println("✓ Refunded: " + insertedCoins);
}
public void reset() {
selectedSlot = null;
balance = 0;
insertedCoins.clear();
}
// ─── Operator / setup ────────────────────────────────────
public void addSlot(String code, Product product, int qty) { slots.put(code, new Slot(product, qty)); }
public void addCoins(int denomination, int count) { coinFloat.merge(denomination, count, Integer::sum); }
}
class VendingMachine:
"""Context — holds current state, inventory, and the money pool."""
def __init__(self) -> None:
# States (created once, reused)
self._idle = IdleState(self)
self._has_selection = HasSelectionState(self)
self._has_money = HasMoneyState(self)
self._dispense_state = DispenseState(self)
self._current: VendingMachineState = self._idle
# Data
self._slots: dict[str, Slot] = {}
self._coin_float: dict[int, int] = {} # denomination → count
self._change_strategy: ChangeStrategy = GreedyChangeStrategy()
# In-flight transaction
self._selected_slot: Slot | None = None
self._balance: int = 0 # money inserted this transaction (paise)
self._inserted_coins: list[int] = []
# ─── Public actions: delegate to current state ───────────
def select_product(self, code: str) -> None:
self._current.select_product(code)
def dispense(self) -> None:
self._current.dispense()
def cancel(self) -> None:
self._current.cancel()
def insert_money(self, denomination: int) -> None:
self._inserted_coins.append(denomination)
self._current.insert_money(denomination)
# ─── State accessors (used by states) ────────────────────
@property
def idle_state(self) -> VendingMachineState:
return self._idle
@property
def has_selection_state(self) -> VendingMachineState:
return self._has_selection
@property
def has_money_state(self) -> VendingMachineState:
return self._has_money
@property
def dispense_state(self) -> VendingMachineState:
return self._dispense_state
def set_state(self, state: VendingMachineState) -> None:
self._current = state
def current_state_name(self) -> str:
return self._current.name()
# ─── Transaction data (used by states) ───────────────────
def get_slot(self, code: str) -> Slot | None:
return self._slots.get(code)
@property
def selected_slot(self) -> Slot | None:
return self._selected_slot
@selected_slot.setter
def selected_slot(self, slot: Slot | None) -> None:
self._selected_slot = slot
@property
def balance(self) -> int:
return self._balance
def add_to_balance(self, amount: int) -> None:
self._balance += amount
@property
def change_strategy(self) -> ChangeStrategy:
return self._change_strategy
@change_strategy.setter
def change_strategy(self, strategy: ChangeStrategy) -> None:
self._change_strategy = strategy
@property
def coin_float(self) -> dict[int, int]:
return self._coin_float
def bank_balance(self) -> None:
"""Inserted coins become part of the float (sale committed)."""
for c in self._inserted_coins:
self._coin_float[c] = self._coin_float.get(c, 0) + 1
def refund_balance(self) -> None:
"""Give the inserted coins back to the user."""
if self._inserted_coins:
print(f"✓ Refunded: {self._inserted_coins}")
def reset(self) -> None:
self._selected_slot = None
self._balance = 0
self._inserted_coins.clear()
# ─── Operator / setup ────────────────────────────────────
def add_slot(self, code: str, product: Product, qty: int) -> None:
self._slots[code] = Slot(product, qty)
def add_coins(self, denomination: int, count: int) -> None:
self._coin_float[denomination] = self._coin_float.get(denomination, 0) + count
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <memory>
#include <iostream>
#include "Product.hpp"
#include "Slot.hpp"
#include "ChangeStrategy.hpp"
#include "GreedyChangeStrategy.hpp"
#include "VendingMachineState.hpp"
#include "IdleState.hpp"
#include "HasSelectionState.hpp"
#include "HasMoneyState.hpp"
#include "DispenseState.hpp"
class VendingMachine {
// States (created once, reused)
std::unique_ptr<VendingMachineState> idle_;
std::unique_ptr<VendingMachineState> hasSelection_;
std::unique_ptr<VendingMachineState> hasMoney_;
std::unique_ptr<VendingMachineState> dispenseState_;
VendingMachineState* current_;
// Data
std::unordered_map<std::string, Slot> slots_;
std::unordered_map<int, int> coinFloat_; // denomination → count
std::unique_ptr<ChangeStrategy> changeStrategy_;
// In-flight transaction
Slot* selectedSlot_ = nullptr;
int balance_ = 0;
std::vector<int> insertedCoins_;
public:
VendingMachine()
: idle_(std::make_unique<IdleState>(*this)),
hasSelection_(std::make_unique<HasSelectionState>(*this)),
hasMoney_(std::make_unique<HasMoneyState>(*this)),
dispenseState_(std::make_unique<DispenseState>(*this)),
current_(idle_.get()),
changeStrategy_(std::make_unique<GreedyChangeStrategy>()) {}
// ─── Public actions ──────────────────────────────────────
void selectProduct(const std::string& code) { current_->selectProduct(code); }
void dispense() { current_->dispense(); }
void cancel() { current_->cancel(); }
void insertMoney(int denomination) {
insertedCoins_.push_back(denomination);
current_->insertMoney(denomination);
}
// ─── State accessors ─────────────────────────────────────
VendingMachineState* getIdleState() { return idle_.get(); }
VendingMachineState* getHasSelectionState() { return hasSelection_.get(); }
VendingMachineState* getHasMoneyState() { return hasMoney_.get(); }
VendingMachineState* getDispenseState() { return dispenseState_.get(); }
void setState(VendingMachineState* s) { current_ = s; }
std::string currentStateName() const { return current_->name(); }
// ─── Transaction data ────────────────────────────────────
Slot* getSlot(const std::string& code) {
auto it = slots_.find(code);
return it != slots_.end() ? &it->second : nullptr;
}
Slot* getSelectedSlot() { return selectedSlot_; }
void setSelectedSlot(Slot* s) { selectedSlot_ = s; }
int getBalance() const { return balance_; }
void addToBalance(int amount) { balance_ += amount; }
ChangeStrategy* getChangeStrategy() { return changeStrategy_.get(); }
std::unordered_map<int, int>& getCoinFloat() { return coinFloat_; }
void bankBalance() {
for (int c : insertedCoins_) coinFloat_[c]++;
}
void refundBalance() {
if (!insertedCoins_.empty()) {
std::cout << "✓ Refunded: [";
for (size_t i = 0; i < insertedCoins_.size(); ++i) {
if (i > 0) std::cout << ", ";
std::cout << insertedCoins_[i];
}
std::cout << "]\n";
}
}
void reset() {
selectedSlot_ = nullptr;
balance_ = 0;
insertedCoins_.clear();
}
// ─── Operator / setup ────────────────────────────────────
void addSlot(const std::string& code, Product product, int qty) {
slots_.emplace(code, Slot(std::move(product), qty));
}
void addCoins(int denomination, int count) { coinFloat_[denomination] += count; }
};
class VendingMachine {
// States (created once, reused)
#idle;
#hasSelection;
#hasMoney;
#dispenseState;
#current;
// Data
#slots = new Map();
#coinFloat = new Map(); // denomination → count
#changeStrategy;
// In-flight transaction
#selectedSlot = null;
#balance = 0;
#insertedCoins = [];
constructor() {
this.#idle = new IdleState(this);
this.#hasSelection = new HasSelectionState(this);
this.#hasMoney = new HasMoneyState(this);
this.#dispenseState = new DispenseState(this);
this.#current = this.#idle;
this.#changeStrategy = new GreedyChangeStrategy();
}
// ─── Public actions: delegate to current state ───────────
selectProduct(code) { this.#current.selectProduct(code); }
dispense() { this.#current.dispense(); }
cancel() { this.#current.cancel(); }
insertMoney(denomination) {
this.#insertedCoins.push(denomination);
this.#current.insertMoney(denomination);
}
// ─── State accessors (used by states) ────────────────────
get idleState() { return this.#idle; }
get hasSelectionState() { return this.#hasSelection; }
get hasMoneyState() { return this.#hasMoney; }
get dispenseState() { return this.#dispenseState; }
setState(s) { this.#current = s; }
get currentStateName() { return this.#current.stateName; }
// ─── Transaction data (used by states) ───────────────────
getSlot(code) { return this.#slots.get(code) || null; }
get selectedSlot() { return this.#selectedSlot; }
set selectedSlot(s) { this.#selectedSlot = s; }
get balance() { return this.#balance; }
addToBalance(amount) { this.#balance += amount; }
get changeStrategy() { return this.#changeStrategy; }
set changeStrategy(s) { this.#changeStrategy = s; }
get coinFloat() { return this.#coinFloat; }
bankBalance() {
for (const c of this.#insertedCoins) {
this.#coinFloat.set(c, (this.#coinFloat.get(c) || 0) + 1);
}
}
refundBalance() {
if (this.#insertedCoins.length > 0)
console.log(`✓ Refunded: [${this.#insertedCoins}]`);
}
reset() {
this.#selectedSlot = null;
this.#balance = 0;
this.#insertedCoins = [];
}
// ─── Operator / setup ────────────────────────────────────
addSlot(code, product, qty) { this.#slots.set(code, new Slot(product, qty)); }
addCoins(denomination, count) {
this.#coinFloat.set(denomination, (this.#coinFloat.get(denomination) || 0) + count);
}
}
Demo.java (Runnable end-to-end)
Drives the machine through a happy path, an out-of-stock rejection, a cancel-and-refund, and a can’t-make-change refund — proving the state transitions and money handling all hold together.
package vending;
import vending.model.Product;
public class Demo {
public static void main(String[] args) {
VendingMachine m = new VendingMachine();
// Prices in paise: ₹25.00 = 2500
m.addSlot("A1", new Product("Coke", 2500), 2);
m.addSlot("A2", new Product("Water", 2000), 0); // out of stock
m.addSlot("B1", new Product("Chips", 3000), 5);
m.addCoins(500, 5); // ₹5 x5
m.addCoins(1000, 5); // ₹10 x5
System.out.println("=== Happy path: buy Coke with exact-ish money ===");
m.selectProduct("A1"); // → HAS_SELECTION
m.insertMoney(1000); // ₹10
m.insertMoney(1000); // ₹20
m.insertMoney(1000); // ₹30 → HAS_MONEY
m.dispense(); // dispense Coke, return ₹5 change → IDLE
System.out.println("\n=== Out of stock ===");
m.selectProduct("A2"); // rejected, stays IDLE
System.out.println("\n=== Cancel and refund ===");
m.selectProduct("B1");
m.insertMoney(1000);
m.cancel(); // refund ₹10 → IDLE
System.out.println("\n=== State after transactions ===");
System.out.println("Current state: " + m.currentStateName());
System.out.println("\n=== Cannot make change (drain the float) ===");
// Empty the float of small coins, then overpay so change is impossible.
VendingMachine m2 = new VendingMachine();
m2.addSlot("C1", new Product("Gum", 1500), 1); // ₹15
m2.addCoins(1000, 0); // no coins at all
m2.selectProduct("C1");
m2.insertMoney(1000);
m2.insertMoney(1000); // ₹20 in, owes ₹5 change but float is empty
m2.dispense(); // cannot make change → refund, stays consistent
}
}
def main() -> None:
m = VendingMachine()
# Prices in paise: ₹25.00 = 2500
m.add_slot("A1", Product("Coke", 2500), 2)
m.add_slot("A2", Product("Water", 2000), 0) # out of stock
m.add_slot("B1", Product("Chips", 3000), 5)
m.add_coins(500, 5) # ₹5 x5
m.add_coins(1000, 5) # ₹10 x5
print("=== Happy path: buy Coke with exact-ish money ===")
m.select_product("A1") # → HAS_SELECTION
m.insert_money(1000) # ₹10
m.insert_money(1000) # ₹20
m.insert_money(1000) # ₹30 → HAS_MONEY
m.dispense() # dispense Coke, return ₹5 change → IDLE
print("\n=== Out of stock ===")
m.select_product("A2") # rejected, stays IDLE
print("\n=== Cancel and refund ===")
m.select_product("B1")
m.insert_money(1000)
m.cancel() # refund ₹10 → IDLE
print("\n=== State after transactions ===")
print(f"Current state: {m.current_state_name()}")
print("\n=== Cannot make change (drain the float) ===")
# Empty the float of small coins, then overpay so change is impossible.
m2 = VendingMachine()
m2.add_slot("C1", Product("Gum", 1500), 1) # ₹15
m2.add_coins(1000, 0) # no coins at all
m2.select_product("C1")
m2.insert_money(1000)
m2.insert_money(1000) # ₹20 in, owes ₹5 change but float is empty
m2.dispense() # cannot make change → refund, stays consistent
if __name__ == "__main__":
main()
#include <iostream>
#include "VendingMachine.hpp"
int main() {
VendingMachine m;
// Prices in paise: ₹25.00 = 2500
m.addSlot("A1", Product("Coke", 2500), 2);
m.addSlot("A2", Product("Water", 2000), 0); // out of stock
m.addSlot("B1", Product("Chips", 3000), 5);
m.addCoins(500, 5); // ₹5 x5
m.addCoins(1000, 5); // ₹10 x5
std::cout << "=== Happy path: buy Coke with exact-ish money ===\n";
m.selectProduct("A1"); // → HAS_SELECTION
m.insertMoney(1000); // ₹10
m.insertMoney(1000); // ₹20
m.insertMoney(1000); // ₹30 → HAS_MONEY
m.dispense(); // dispense Coke, return ₹5 change → IDLE
std::cout << "\n=== Out of stock ===\n";
m.selectProduct("A2"); // rejected, stays IDLE
std::cout << "\n=== Cancel and refund ===\n";
m.selectProduct("B1");
m.insertMoney(1000);
m.cancel(); // refund ₹10 → IDLE
std::cout << "\n=== State after transactions ===\n";
std::cout << "Current state: " << m.currentStateName() << "\n";
std::cout << "\n=== Cannot make change (drain the float) ===\n";
VendingMachine m2;
m2.addSlot("C1", Product("Gum", 1500), 1); // ₹15
m2.addCoins(1000, 0); // no coins at all
m2.selectProduct("C1");
m2.insertMoney(1000);
m2.insertMoney(1000); // ₹20 in, owes ₹5 change but float is empty
m2.dispense(); // cannot make change → refund, stays consistent
return 0;
}
function main() {
const m = new VendingMachine();
// Prices in paise: ₹25.00 = 2500
m.addSlot("A1", new Product("Coke", 2500), 2);
m.addSlot("A2", new Product("Water", 2000), 0); // out of stock
m.addSlot("B1", new Product("Chips", 3000), 5);
m.addCoins(500, 5); // ₹5 x5
m.addCoins(1000, 5); // ₹10 x5
console.log("=== Happy path: buy Coke with exact-ish money ===");
m.selectProduct("A1"); // → HAS_SELECTION
m.insertMoney(1000); // ₹10
m.insertMoney(1000); // ₹20
m.insertMoney(1000); // ₹30 → HAS_MONEY
m.dispense(); // dispense Coke, return ₹5 change → IDLE
console.log("\n=== Out of stock ===");
m.selectProduct("A2"); // rejected, stays IDLE
console.log("\n=== Cancel and refund ===");
m.selectProduct("B1");
m.insertMoney(1000);
m.cancel(); // refund ₹10 → IDLE
console.log("\n=== State after transactions ===");
console.log(`Current state: ${m.currentStateName}`);
console.log("\n=== Cannot make change (drain the float) ===");
const m2 = new VendingMachine();
m2.addSlot("C1", new Product("Gum", 1500), 1); // ₹15
m2.addCoins(1000, 0); // no coins at all
m2.selectProduct("C1");
m2.insertMoney(1000);
m2.insertMoney(1000); // ₹20 in, owes ₹5 change but float is empty
m2.dispense(); // cannot make change → refund, stays consistent
}
main();
Sequence Diagram — Buy a Product
sequenceDiagram
participant User
participant VM as VendingMachine
participant S as Current State
participant CS as ChangeStrategy
User->>VM: selectProduct("A1")
VM->>S: selectProduct("A1") [IdleState]
S->>VM: setState(HasSelection)
User->>VM: insertMoney(x3)
VM->>S: insertMoney() [HasSelectionState]
S->>VM: setState(HasMoney) when balance ≥ price
User->>VM: dispense()
VM->>S: dispense() [DispenseState]
S->>CS: makeChange(overpaid, float)
CS-->>S: [denominations]
S->>VM: dispenseOne() + bankBalance()
S->>VM: setState(Idle)
VM-->>User: product + change
How to Extend
| Extension | Implementation |
|---|---|
| Card / UPI payment | New insertMoney-equivalent action; a PaymentStrategy alongside the states |
| Maintenance mode | New MaintenanceState that rejects all customer actions; operator toggles it |
| Optimal change | Swap in a DPChangeStrategy — DP over denominations for non-canonical sets |
| Multi-item cart | Track a list of selected slots; sum prices before the money state |
| Low-stock alerts | Observer on Slot.dispenseOne() notifying an operator dashboard |
| Audit log | Decorator around each state logging entry/exit |
What Interviewers Look For
- ✅ State pattern — one class per state, no
if (state == X)ladders in the machine - ✅ Invalid transitions rejected cleanly — dispensing before paying returns a message, not a crash
- ✅ Money as integers — paise/cents, never
double - ✅ Change computed before committing — inventory/float stay consistent on failure
- ✅ Refund correctness — cancel and can’t-make-change both leave the machine unchanged
- ✅ Strategy for change — swappable algorithm, not hardcoded greedy logic
- ✅ Runnable demo — happy path, out-of-stock, cancel, and change-failure all shown
Related Designs
- Snake & Ladder — turn/state modelling in a game loop
- Parking Lot — Strategy pattern for swappable pricing
- Elevator System — State + Strategy for request scheduling
Discussion
Newest first