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

  1. Machine holds multiple products, each in a slot with a price and a quantity.
  2. User flow: select a product → insert money → collect product + change.
  3. Accept money in fixed denominations (coins and notes).
  4. Return change when the user overpays, using available denominations.
  5. Allow the user to cancel and get a full refund at any point before dispensing.
  6. Reject selection when the product is out of stock or the machine can’t make change.
  7. Operator can refill inventory and restock the coin float.

Non-Functional Requirements

  1. No invalid transitions — you can’t dispense before paying, can’t pay before selecting.
  2. Never over-dispense — money and inventory stay consistent even on cancel.
  3. 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

  1. State pattern — one class per state, no if (state == X) ladders in the machine
  2. Invalid transitions rejected cleanly — dispensing before paying returns a message, not a crash
  3. Money as integers — paise/cents, never double
  4. Change computed before committing — inventory/float stay consistent on failure
  5. Refund correctness — cancel and can’t-make-change both leave the machine unchanged
  6. Strategy for change — swappable algorithm, not hardcoded greedy logic
  7. Runnable demo — happy path, out-of-stock, cancel, and change-failure all shown

Discussion

Newest first
You

Free system design + DSA prep. If it helped you crack an interview, consider supporting.