Vending Machine
The textbook demo of the State pattern, and for good reason: a vending machine really does mean something different by "insert coin" depending on whether it's waiting for money, already holding some, or mid-dispense. Model that literally - one class per state - and the machine practically writes itself.
Requirements
Functional
- A customer inserts coins, selects a product by code, and either gets the product plus any change, or gets a rejection (insufficient funds, sold-out slot) with coins returned.
- Inserting more coins after already having enough should still work - the machine keeps a running balance, not a single coin.
- Selecting a product it doesn't have in stock returns the coins already inserted.
Non-functional
- The set of states (and the rules for what's legal in each) should be easy to extend - a "maintenance" state that rejects everything is a plausible future ask.
- Coin handling and product/inventory management are separate concerns; neither state class should need to know how the other is implemented.
Design
VendingMachine is the context; it holds a VendingState and forwards every customer
action - insertCoin, selectProduct, dispense - to whichever state object it currently
holds. Every state implements the same three methods, and most of them implement two of the
three as "that's not legal right now."
- 1The machine forwards this straight to its current state - right now, IdleState.
- 2IdleState records the balance and swaps the machine into HasMoneyState.
- 3The customer picks a slot.
- 4HasMoneyState is the only state where a selection can possibly succeed.
- 5Stock and pricing are Inventory’s job, not the state’s - it just asks.
- 6Enough balance and stock: the state swaps itself for DispensingState.
- 7DispensingState releases the product, returns change, and resets the machine to IdleState.
Class diagram
implementsuses
Code
Design decisions
- State pattern earns its keep here. Unlike the ATM (four transitions, little per-state behavior), "what does insertCoin mean" genuinely differs in each of these three states - accumulate a new balance, add to an existing one, or reject outright because a dispense is already underway. That's exactly the shape the pattern exists for.
Inventoryis not aVendingStateresponsibility. Stock levels and prices live in their own class that any state can query, so "is this slot sold out" is answered in one place instead of duplicated acrossHasMoneyStateand wherever else might need it.- Change calculation lives in
DispensingState, notVendingMachine. The moment of dispensing is also the moment change is owed, so keeping that math next to the state that triggers it avoids aVendingMachine.calculateChange()method nothing else calls. - What's missing for a real system: exact-change tracking (a real machine refuses a sale it can't make change for, which means modeling the coin inventory the machine is holding, not just the customer's balance) and a coin-jam / sensor-fault path, which would be a good candidate for the maintenance state mentioned above.