Machine Coding Problem

Vending Machine

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

Requirements & System Scope

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.

Class Diagram & Entity Relationships

Structural layout showing clean separation of state entities and context classes:

Loading...
  • State Context Pattern: VendingMachine holds the active State reference, delegating all operations to polymorphic state methods.
  • Decoupled Product Management: Product handles thread-safe quantites internally without leaking inventory state.

Design Patterns & SOLID Principles

  • State Pattern (Vending Cycle): Polymorphic states represent different machine lifecycles, separating code blocks and eliminating nested conditional checks.
  • Greedy Strategy Pattern (Change Return): Iteratively calculates exact change based on available cash box counts, returning optimal coin distributions.
  • Single Responsibility Principle (SRP): Each state class is strictly responsible for handling its single lifecycle state boundaries, making adding states (e.g. maintenance) simple.

Core Execution Workflows

Vending State Traversal

  1. Machine initializes in IdleState.
  2. User inserts coins:
    1. IdleState adds coin amounts, transitioning to HasMoneyState.
  3. User selects item:
    1. If stock is valid and balance is sufficient, transition to DispensingState.
  4. Item Dispense Tick:
    1. Decrement product inventory.
    2. Calculate optimal change and execute the coin box dispensation strategy.
    3. Clear active session balances, resetting back to IdleState (or OutOfStockState if completely out).

Concurrency & Thread Safety Strategy

Vending machines must act as single-user entry terminals. Concurrent coin insertion and product selection from parallel threads can corrupt states:

  • Exclusive Lock Synchronization: Wrap all outer entry methods (insert coin, select product, cancel) inside exclusive lock blocks, guaranteeing consistent transactions.
  • Internal Helper Decoupling (Deadlock Prevention): Avoid calling public lock-protected API methods (like dispense()) from within other locked methods (like selectProduct()). Use internal, non-locking helper functions (e.g., _dispense_internal()) to eliminate potential deadlock conditions on non-reentrant locks (such as Python's standard Lock).
  • Atomic Stock Modification: Thread-safe inventory collections and localized locking on product entries protect against double-allocation on empty slots.

Complete Clean Code Blueprint

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

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...