Skip to content
FollowtheDamnCode

LLD: Vending Machine

Coins in, product out, change back — a textbook State-pattern design where every button means something different depending on what the machine is doing.

Big idea

A vending machine is a state machine wrapped around an inventory and a cash box. The same action — insert a coin, press a product, cancel — means different things when the machine is idle, holding money, or dispensing. Model each situation as a State object and the illegal cases ("dispense without paying") handle themselves.

Requirements

Functional

  • Products in slots, each with a price and a count.
  • Accept coins of fixed denominations; show the balance.
  • Select a product: dispense it if paid enough and in stock, return change.
  • Cancel: refund the inserted coins.
  • An operator can restock and collect cash.

Rules: can't select before inserting money; can't dispense a sold-out product; change must be makeable from the coins in the machine — otherwise refuse the sale and keep the customer's money safe.

Out of scope: card payments, network reporting (extensions).

Entities

VendingMachine (the context), State with Idle, HasMoney, Dispensing (plus OutOfService as an extension), Inventory (slot → product, count), CoinBox (counts per denomination, makes change), Product.

Think of it like

A ticket desk with a clerk who follows a strict script. Before you pay, "I'd like a ticket" gets "please pay first". Once you've paid, it gets you the ticket and your change. While the printer is running, the clerk ignores every other request until it's done.

Class diagram

States, inventory, and a coin box
Machine lifecycle

Key flow: a purchase with change

Buying a 120 drink with two 100 coins

Code skeleton

A machine that refuses what it can't honour
class CoinBox:
    def __init__(self, counts):
        self.counts = dict(counts)                     # denomination -> count
    def add(self, coins):
        for c in coins:
            self.counts[c] = self.counts.get(c, 0) + 1
    def make_change(self, amount):                     # greedy works for these denominations
        change, counts = [], dict(self.counts)
        for coin in sorted(counts, reverse=True):
            while amount >= coin and counts[coin] > 0:
                amount -= coin; counts[coin] -= 1; change.append(coin)
        if amount:
            return None                                # can't make exact change
        self.counts = counts
        return change


class Idle:
    def insert(self, vm, coin):
        vm.inserted.append(coin); vm.state = HasMoney()
        return f"balance {sum(vm.inserted)}"
    def select(self, vm, slot):
        return "insert money first"
    def cancel(self, vm):
        return "nothing to refund"

class HasMoney:
    def insert(self, vm, coin):
        vm.inserted.append(coin)
        return f"balance {sum(vm.inserted)}"
    def select(self, vm, slot):
        name, price, count = vm.slots[slot]
        paid = sum(vm.inserted)
        if count == 0:
            return f"{name} sold out"
        if paid < price:
            return f"{name} costs {price}, balance {paid}"
        vm.coins.add(vm.inserted)                      # take the money in first...
        change = vm.coins.make_change(paid - price)
        if change is None:                             # ...and give it back if change is impossible
            vm.coins.make_change(paid)
            return self.cancel(vm) + " (no change available)"
        vm.slots[slot] = (name, price, count - 1)
        vm.inserted, vm.state = [], Idle()
        return f"dispensed {name}, change {change}"
    def cancel(self, vm):
        refund, vm.inserted, vm.state = vm.inserted, [], Idle()
        return f"refunded {refund}"


class VendingMachine:
    def __init__(self, slots, coins):
        self.slots, self.coins = slots, CoinBox(coins)
        self.state, self.inserted = Idle(), []
    def insert(self, coin):  return self.state.insert(self, coin)
    def select(self, slot):  return self.state.select(self, slot)
    def cancel(self):        return self.state.cancel(self)


vm = VendingMachine({"A1": ("cola", 120, 2), "B1": ("water", 60, 0)}, {50: 1, 20: 2, 10: 1})
for action, arg in [("select", "A1"), ("insert", 100), ("select", "A1"), ("insert", 100),
                    ("select", "A1"), ("insert", 50), ("select", "B1"), ("cancel", None),
                    ("insert", 100), ("insert", 100), ("select", "A1")]:
    result = getattr(vm, action)(arg) if arg is not None else getattr(vm, action)()
    print(f"{action:6} {str(arg or ''):4} -> {result}")
output
select A1   -> insert money first
insert 100  -> balance 100
select A1   -> cola costs 120, balance 100
insert 100  -> balance 200
select A1   -> dispensed cola, change [50, 20, 10]
insert 50   -> balance 50
select B1   -> water sold out
cancel      -> refunded [50]
insert 100  -> balance 100
insert 100  -> balance 200
select A1   -> refunded [100, 100] (no change available)

Run and edit it in the app.

The last purchase is refused because the machine has run out of the coins needed for 80 change — and the customer's 200 comes back instead of being swallowed.

Watch it

Why model the vending machine with the State pattern instead of if-checks on a status field?

A customer pays 200 for a 120 item but the machine can't make 80 in change. What should happen?

Extensions and trade-offs

  • Out of service — a fourth state entered when empty or on a fault; every action answers "out of order".
  • Card payments — a PaymentMethod Strategy; coins become one implementation.
  • Change-making — greedy is correct for common coin sets but not all; a dynamic-programming solver is the general answer.
  • Telemetry — an Observer emitting "sold", "low stock", "coin box full".
  • Concurrency — one user at a time physically, but a remote "restock" call must lock the inventory.

Key terms

  • Context — The vending machine object that delegates every action to its current state.
  • Invariant — Here, money in equals product out plus change, or a full refund.
  • Greedy change-making — Giving the largest coin possible at each step; right for standard coin systems.