Machine Coding Problem

Vending Machine

maco30maco60macoAllutilitystate-pattern-(idle/dispense)
Commonly Asked By:GoogleAmazonMicrosoft

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());
    }
}