Functional Scope (In-Scope)
- Denomination Ingestion: Accept dynamic values (Nickel, Dime, Quarter, Dollar) and accumulate balance safely.
- Dynamic Product Dispense: Manage stocks, dispense items, and update physical amounts on successful purchases.
- Greedy Change Dispensation: Provide change return capabilities checking physical cash enums availability.
- Restocking Administration: Support administrative reloading routines that allow thread-safe item restocks.
Explicit Boundaries (Out-of-Scope)
- No Hardware / Physical Sensor Interfacing: Modeled purely using robust software classes.
- No Long-term Credit Ledger: Session based transactions only, clearing balance once items are released.
Clean reference designs showing transactional state transitions and inventory updates in Java and Python:
// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
enum Coin {
NICKEL(0.05), DIME(0.10), QUARTER(0.25), DOLLAR(1.00);
private final double value;
Coin(double value) { this.value = value; }
public double getValue() { return value; }
}
interface State {
void insertCoin(Coin coin);
void selectProduct(String productCode);
void dispense();
void cancel();
}
class Product {
private final String code;
private final double price;
private int quantity;
public Product(String code, double price, int quantity) {
this.code = code;
this.price = price;
this.quantity = quantity;
}
public String getCode() { return code; }
public double getPrice() { return price; }
public synchronized int getQuantity() { return quantity; }
public synchronized void decrement() { quantity--; }
public synchronized void restock(int qty) { quantity += qty; }
}
class VendingMachine {
private final Map<String, Product> inventory = new ConcurrentHashMap<>();
private final Map<Coin, Integer> coinInventory = new ConcurrentHashMap<>();
private double currentBalance = 0.0;
private State currentState;
private String selectedProductCode;
private final ReentrantLock lock = new ReentrantLock();
private final State idleState;
private final State hasMoneyState;
private final State dispensingState;
private final State outOfStockState;
public VendingMachine() {
idleState = new IdleState(this);
hasMoneyState = new HasMoneyState(this);
dispensingState = new DispensingState(this);
outOfStockState = new OutOfStockState(this);
currentState = idleState;
// Populate initial change coins
coinInventory.put(Coin.NICKEL, 20);
coinInventory.put(Coin.DIME, 10);
coinInventory.put(Coin.QUARTER, 10);
coinInventory.put(Coin.DOLLAR, 5);
}
public void addProduct(Product p) { inventory.put(p.getCode(), p); }
public Product getProduct(String code) { return inventory.get(code); }
public Collection<Product> getAllProducts() { return inventory.values(); }
public ReentrantLock getLock() { return lock; }
public void setState(State state) { this.currentState = state; }
public double getBalance() { return currentBalance; }
public void addBalance(double amt) { currentBalance = Math.round((currentBalance + amt) * 100.0) / 100.0; }
public void clearBalance() { currentBalance = 0.0; }
public State getIdleState() { return idleState; }
public State getHasMoneyState() { return hasMoneyState; }
public State getDispensingState() { return dispensingState; }
public State getOutOfStockState() { return outOfStockState; }
public void setSelectedProduct(String code) { this.selectedProductCode = code; }
public String getSelectedProduct() { return selectedProductCode; }
public void insertCoin(Coin coin) {
lock.lock();
try {
currentState.insertCoin(coin);
} finally {
lock.unlock();
}
}
public void selectProduct(String productCode) {
lock.lock();
try {
currentState.selectProduct(productCode);
} finally {
lock.unlock();
}
}
void dispenseInternal() {
currentState.dispense();
}
public void dispense() {
lock.lock();
try {
dispenseInternal();
} finally {
lock.unlock();
}
}
public void cancel() {
lock.lock();
try {
currentState.cancel();
} finally {
lock.unlock();
}
}
public void addCoins(Coin coin, int count) {
coinInventory.put(coin, coinInventory.getOrDefault(coin, 0) + count);
}
public boolean returnChange(double changeAmount) {
double remaining = Math.round(changeAmount * 100.0) / 100.0;
if (remaining == 0.0) return true;
Map<Coin, Integer> dispensed = new HashMap<>();
Coin[] denominations = {Coin.DOLLAR, Coin.QUARTER, Coin.DIME, Coin.NICKEL};
for (Coin c : denominations) {
int needed = (int) (remaining / c.getValue());
if (needed > 0) {
int available = coinInventory.getOrDefault(c, 0);
int take = Math.min(needed, available);
if (take > 0) {
dispensed.put(c, take);
remaining = Math.round((remaining - take * c.getValue()) * 100.0) / 100.0;
}
}
}
if (remaining > 0.0) {
System.out.println("System Alert: Insufficient physical coins to return change!");
return false;
}
// Deduct from inventory
for (Map.Entry<Coin, Integer> entry : dispensed.entrySet()) {
coinInventory.put(entry.getKey(), coinInventory.get(entry.getKey()) - entry.getValue());
System.out.println("Returned change coin: " + entry.getKey() + " x" + entry.getValue());
}
return true;
}
}
class IdleState implements State {
private final VendingMachine machine;
public IdleState(VendingMachine machine) { this.machine = machine; }
public void insertCoin(Coin coin) {
machine.addBalance(coin.getValue());
System.out.println("Inserted coin " + coin + ". Balance: $" + machine.getBalance());
machine.setState(machine.getHasMoneyState());
}
public void selectProduct(String code) { System.out.println("Insert coins first before choosing a product."); }
public void dispense() { System.out.println("No coins loaded."); }
public void cancel() { System.out.println("No transactional balance to cancel."); }
}
class HasMoneyState implements State {
private final VendingMachine machine;
public HasMoneyState(VendingMachine machine) { this.machine = machine; }
public void insertCoin(Coin coin) {
machine.addBalance(coin.getValue());
System.out.println("Inserted another coin " + coin + ". Balance: $" + machine.getBalance());
}
public void selectProduct(String code) {
Product p = machine.getProduct(code);
if (p == null || p.getQuantity() <= 0) {
System.out.println("Product " + code + " is out of stock.");
return;
}
if (machine.getBalance() < p.getPrice()) {
System.out.println("Insufficient funds! Needs $" + p.getPrice() + ", current: $" + machine.getBalance());
return;
}
machine.setSelectedProduct(code);
machine.setState(machine.getDispensingState());
System.out.println("Product " + p.getCode() + " selected. Transitioning to dispensing.");
machine.dispenseInternal(); // Auto-dispense upon valid selection
}
public void dispense() { System.out.println("Select a product to initiate dispense."); }
public void cancel() {
double change = machine.getBalance();
System.out.println("Transaction cancelled. Returning change: $" + change);
machine.returnChange(change);
machine.clearBalance();
machine.setState(machine.getIdleState());
}
}
class DispensingState implements State {
private final VendingMachine machine;
public DispensingState(VendingMachine machine) { this.machine = machine; }
public void insertCoin(Coin coin) { System.out.println("Currently dispensing, please wait."); }
public void selectProduct(String code) { System.out.println("Currently dispensing, please wait."); }
public void dispense() {
Product p = machine.getProduct(machine.getSelectedProduct());
p.decrement();
double change = Math.round((machine.getBalance() - p.getPrice()) * 100.0) / 100.0;
System.out.println("Dispensing product " + p.getCode() + " ($" + p.getPrice() + ")");
if (change > 0) {
System.out.println("Change due: $" + change);
boolean success = machine.returnChange(change);
if (!success) {
System.out.println("Refunding complete amount...");
machine.returnChange(machine.getBalance());
p.restock(1); // rollback
}
}
machine.clearBalance();
machine.setSelectedProduct(null);
// Check overall machine stock
boolean allOut = true;
for (Product prod : machine.getAllProducts()) {
if (prod.getQuantity() > 0) {
allOut = false;
break;
}
}
machine.setState(allOut ? machine.getOutOfStockState() : machine.getIdleState());
}
public void cancel() { System.out.println("Cannot cancel while dispensing is in progress."); }
}
class OutOfStockState implements State {
private final VendingMachine machine;
public OutOfStockState(VendingMachine machine) { this.machine = machine; }
public void insertCoin(Coin coin) { System.out.println("Machine is completely Out of Stock."); }
public void selectProduct(String code) { System.out.println("Machine is completely Out of Stock."); }
public void dispense() { System.out.println("Nothing to dispense."); }
public void cancel() { System.out.println("Nothing to cancel."); }
}
// ─── DRIVER CLASS ──────────────────────────────────────────────────────────
public class VendingMachineDriver {
public static void main(String[] args) {
System.out.println("=== VENDING MACHINE STATE PATTERN DRIVER ===");
VendingMachine machine = new VendingMachine();
machine.addProduct(new Product("Coke", 1.25, 2));
machine.addProduct(new Product("Chips", 0.75, 1));
// Scenario 1: Standard Purchase Chips
System.out.println("\n--- Scenario 1: Buying Chips ($0.75) ---");
machine.insertCoin(Coin.QUARTER);
machine.insertCoin(Coin.DOLLAR); // balance = 1.25
machine.selectProduct("Chips");
// Scenario 2: Insufficient funds & Cancel refund
System.out.println("\n--- Scenario 2: Buying Coke ($1.25) with Insufficient Funds, then Cancel ---");
machine.insertCoin(Coin.QUARTER);
machine.insertCoin(Coin.DIME); // balance = 0.35
machine.selectProduct("Coke"); // should reject
machine.cancel(); // should return $0.35
// Scenario 3: Successful Purchase Coke & out of stock transition
System.out.println("\n--- Scenario 3: Successful Coke Purchase & Restock Flow ---");
machine.insertCoin(Coin.DOLLAR);
machine.insertCoin(Coin.QUARTER); // balance = 1.25
machine.selectProduct("Coke");
System.out.println("\n--- Restocking admin simulation ---");
Product coke = machine.getProduct("Coke");
System.out.println("Coke qty: " + coke.getQuantity());
coke.restock(5);
System.out.println("Coke qty after restock: " + coke.getQuantity());
}
}