Design a Vending Machine
State machine design for a vending machine covering State pattern, coin handling, product dispensing, change calculation, and inventory management.
The Problem
You are building the software for a vending machine deployed in office buildings. The current firmware is one giant switch statement on a machineMode integer. Every new coin type or payment option requires editing the same monolithic function, and bugs cascade through unrelated states. Last month a firmware update caused machines to dispense products without collecting payment.
A vending machine is a textbook state machine problem with a small set of states (idle, accepting money, dispensing) and strict transition rules. The interviewer wants to see you model these states explicitly, handle coin arithmetic carefully, and keep the design open for extension.
Design the core classes for a vending machine that handles coin insertion, product selection, dispensing, and change return.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "What coin denominations does the machine accept?"
Interviewer: "Penny (1 cent), nickel (5), dime (10), and quarter (25). No bills for now."
Coins as integers in cents avoids floating-point arithmetic entirely. Now confirm interactions.
You: "Can a user insert multiple coins before selecting a product?"
Interviewer: "Yes. They keep inserting coins until they have enough, then select."
You: "What happens if the user selects a product but hasn't inserted enough money?"
Interviewer: "Show how much more is needed. Don't dispense anything."
You: "What if the selected product is out of stock?"
Interviewer: "Reject the selection and let the user pick something else or cancel for a refund."
Both error cases keep the machine in the same state. Now clarify boundaries.
You: "Should the machine track its own coin inventory for making change, or assume it always has enough?"
Interviewer: "Assume it always has enough. Mention coin inventory as an extension."
That simplifies the change-making algorithm. You only need to compute the correct coins, not check availability.
You: "Does the machine need to handle concurrent users or is it one user at a time?"
Interviewer: "One user at a time. It's a physical machine with one interface."
You: "Should we support cashless payment like credit cards or mobile pay?"
Interviewer: "Coins only for the base design. Cashless is a great extension to mention."
You: "Can the user cancel a transaction and get a full refund at any point?"
Interviewer: "Yes, as long as the machine isn't actively dispensing a product."
Perfect. You have clarified scope and ruled out unnecessary complexity.
Final Requirements
Functional Requirements:
- The machine accepts pennies (1), nickels (5), dimes (10), and quarters (25)
- Users insert one or more coins to build up a balance
- Users select a product by its slot code (e.g., "A1", "B3")
- If the balance covers the price, the machine dispenses the product and returns change
- If the balance is insufficient, the machine tells the user how much more is needed
- Users can cancel at any time to receive a full refund of inserted coins
Non-Functional Requirements:
- The machine transitions through well-defined states: IDLE, HAS_MONEY, DISPENSING
- Adding a new payment method requires a new class, not changes to existing state logic
- Inventory updates are atomic: stock decreases only after a successful dispense
Out of Scope:
- Bill acceptance or cashless payment
- Coin inventory management for making change
- Multiple simultaneous users
- Remote monitoring or telemetry
- UI/display rendering
- Product restocking workflow
Example Inputs and Outputs
Scenario 1: Successful purchase with exact change
- Input: Insert quarter, insert quarter, select product "A1" (price 50 cents)
- Expected: Product "A1" dispensed, no change returned, machine returns to idle
- Why: Validates the happy path through all three states (IDLE to HAS_MONEY to DISPENSING to IDLE)
Scenario 2: Insufficient funds
- Input: Insert dime, select product "B2" (price 75 cents)
- Expected: "Insufficient funds. Insert 65 more cents." Machine stays in HAS_MONEY state
- Why: Validates that the machine rejects the selection without corrupting state
Scenario 3: Cancel and refund
- Input: Insert quarter, insert dime, cancel transaction
- Expected: 35 cents refunded, machine returns to idle with zero balance
- Why: Validates the refund path and state reset
Scenario 4: Out of stock
- Input: Insert quarter x3 (75 cents), select product "C1" (price 50 cents, stock = 0)
- Expected: "Product C1 is out of stock." Machine stays in HAS_MONEY state
- Why: Validates inventory check before dispensing
Try It Yourself
Try it yourself
Before reading the solution, spend 15 minutes sketching a state diagram and listing the core entities. Focus on which actions are valid in which states and how the machine transitions between them. Compare your approach with the walkthrough below.
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look at your requirements and pull out the nouns. A vending machine problem has fewer entities than something like chess, but the state management is where the complexity lives.
A common mistake is cramming everything into a single VendingMachine class. The machine orchestrates, but it delegates state behavior, inventory tracking, and coin arithmetic to focused classes.
| Entity | Responsibility | Key attributes |
|---|---|---|
| VendingMachine | The orchestrator. Delegates user actions to the current state. Holds balance and inventory. | currentState, balance, inventory |
| VendingState | Interface for state-specific behavior. Each state decides what actions are valid. | insertCoin(), selectProduct(), cancel() |
| IdleState | Handles the resting state. Only coin insertion transitions out of idle. | (stateless) |
| HasMoneyState | Handles the state after coins are inserted. Allows selection, more coins, or cancel. | (stateless) |
| DispensingState | Handles the brief dispensing state. Blocks all user input until complete. | (stateless) |
| Product | Value object for a product in the machine. Immutable data holder. | name, price, slotCode |
| Inventory | Tracks stock levels per product. Owns the add/remove/check logic. | stock map, product catalog |
| Coin | Enum representing valid coin denominations with their cent values. | value (in cents) |
Notice that the three state classes are separate from VendingMachine. The machine holds a reference to its current state and delegates actions to it. This is the State pattern: behavior changes based on which state object is active, with no if-else chains.
Interview tip
When listing entities, mention their responsibilites out loud. Saying "Product is a pure data holder, no behavior" shows the interviewer you are thinking about SRP from the start.
Step 2: Define Relationships and Class Design
State Diagram
Before drawing the class diagram, sketch the state machine. This is the most important diagram for a vending machine problem because it shows every valid transition and what triggers it.
Three states, seven transitions. Every action is accounted for. In the interview, draw this first. It takes 30 seconds and proves you understand the problem.
Class Interface Derivation
VendingMachine (the orchestrator)
The machine is the context object in the State pattern. It exposes the public API and delegates every action to whichever state is currently active.
Deriving state from requirements:
| Requirement | What VendingMachine must track |
|---|---|
| "Users insert coins to build up a balance" | Current balance in cents |
| "Machine transitions through IDLE, HAS_MONEY, DISPENSING" | Current state object |
| "Machine dispenses product and returns change" | Reference to inventory |
This gives us the core fields:
balanceCents: int
currentState: VendingState
inventory: Inventory
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Insert coins" | insertCoin(coin) |
| "Select a product" | selectProduct(slotCode) |
| "Cancel for refund" | cancelTransaction() |
| "Return change after purchase" | dispenseAndReturnChange(slotCode) |
The public methods (insertCoin, selectProduct, cancelTransaction) are thin wrappers that delegate to currentState. The machine also exposes internal helpers (addBalance, resetBalance, setState) for state objects to mutate it.
Inventory
Inventory is a standalone service. It knows nothing about states or coins. It answers one question: is product X available?
Deriving state from requirements:
| Requirement | What Inventory must track |
|---|---|
| "Select a product by slot code" | Map of slot code to Product |
| "Out of stock rejection" | Map of slot code to remaining quantity |
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Check if product exists and is in stock" | isAvailable(slotCode) |
| "Get product details for price check" | getProduct(slotCode) |
| "Decrease stock after dispense" | dispense(slotCode) |
| "Load products into machine" | addProduct(product, quantity) |
Step 3: Choose Design Patterns
Pattern: State Pattern (for machine behavior)
The signal: The requirements say the same action (like pressing a product button) does different things depending on the current state. That is the textbook signal for the State pattern.
Why this pattern over alternatives: You could use a single class with an enum field and switch statements. That works for three states, but violates OCP. Every new state or action requires editing the same switch blocks. The State pattern isolates each state's behavior into its own class.
How it maps to our entities:
- Interface =
VendingState - Concrete implementations =
IdleState,HasMoneyState,DispensingState - Context =
VendingMachine(holds the current state and delegates)
I prefer State pattern here even with only three states. In an interview, it demonstrates knowledge of state machine modeling. Each arrow in the state diagram maps directly to a method in a specific state class.
Why not Strategy here?
Strategy and State look similar (both use interface + concrete classes), but they solve different problems. Strategy swaps algorithms chosen by the client. State swaps behavior driven by internal transitions. Here, the machine decides its own state transitions, so State is the correct pattern. If we later add multiple change-calculation algorithms, Strategy would fit there.
Step 4: Build the Solution
Pseudocode for Key Methods
Before writing the full implementation, walk through the two most interesting methods at the whiteboard level.
selectProduct(): The Core Logic
This is the most complex method because it involves validation, state transitions, and coin arithmetic.
Core logic (happy path):
- Look up the product by slot code
- Check if in stock, then compare balance to price
- If sufficient: dispense, calculate change, reset balance, return to IDLE
Edge cases (reject before touching state):
- Product not found, out of stock, or insufficient balance
function handleSelectProduct(machine, slotCode):
product = machine.inventory.getProduct(slotCode)
if product is null: return "Invalid slot"
if not available(slotCode): return "Out of stock"
if machine.balance < product.price:
return "Insert " + (product.price - machine.balance) + " more cents"
// Happy path: dispense
change = machine.balance - product.price
machine.inventory.dispense(slotCode)
machine.resetBalance()
machine.setState(DISPENSING)
coins = calculateChange(change)
machine.setState(IDLE)
return "Dispensed " + product.name + ". Change: " + coins
Notice the three guard clauses. They reject invalid requests before touching any state. Validate first, mutate last.
calculateChange(): Greedy Coin Algorithm
Change calculation uses a greedy algorithm. Start with the largest denomination and work down.
function calculateChange(amountCents):
coins = []
for denomination in [QUARTER, DIME, NICKEL, PENNY]:
while amountCents >= denomination.value:
coins.add(denomination)
amountCents -= denomination.value
return coins
The greedy approach works for US coin denominations because each denomination divides evenly into the larger ones. For arbitrary coin values, you would need dynamic programming, but the interviewer specified standard US coins.
Full Implementation
// Enum of valid coin denominations. Avoids magic
// numbers and makes invalid coins impossible.
public enum Coin {
PENNY(1), NICKEL(5), DIME(10), QUARTER(25);
private final int valueCents;
Coin(int valueCents) { this.valueCents = valueCents; }
public int valueCents() { return valueCents; }
}
Architecture note: The code follows the State pattern precisely. VendingMachine is the context, VendingState is the state interface, and the three concrete states implement all behavior. State objects are created once and reused (flyweight approach) because they hold no instance data. All mutable state lives on VendingMachine. The calculateChange method lives on VendingMachine because both HasMoneyState (for purchase change) and the cancel path (for refunds) need it.
Step 5: Trace a Scenario
Walk through Scenario 1 (successful purchase with exact change) to verify the design handles the full lifecycle.
Step-by-step through the code:
- First quarter.
insertCoin(QUARTER)delegates toIdleState. Adds 25 to balance, transitions toHasMoneyState. - Second quarter. Delegates to
HasMoneyState. Balance goes to 50 cents. State unchanged. - Select "A1".
HasMoneyState.handleSelectProduct()runs guard checks: exists (yes), in stock (yes), 50 >= 50 (yes). All pass. - Dispense. Transitions to
DispensingState, callsinventory.dispense("A1"), resets balance, calculates change (0 = empty list), transitions toIdleState. - Idle. Balance is 0, "A1" stock decreased by 1. Ready for the next user.
Watch the state transition order
Notice we call setState(DispensingState) before dispensing, then setState(IdleState) after. If dispense throws (e.g., mechanical failure), the machine stays in DispensingState rather than silently returning to idle.
Extensibility
1. "How would you add cashless payment (credit cards, mobile pay)?"
"Today the machine only handles coins through the State pattern. If we add cashless payment, I would extract a PaymentMethod interface with a pay(int amountCents) method. CoinPayment becomes one implementation. CardPayment and MobilePayment become new implementations. The state classes don't change at all because they already ask the machine for the balance. The only change is how the balance gets funded."
interface PaymentMethod {
void addFunds(VendingMachine machine, int cents);
void refund(VendingMachine machine, int cents);
}
class CoinPayment implements PaymentMethod { ... }
class CardPayment implements PaymentMethod { ... }
This is the Strategy pattern layered on top of State. OCP keeps the existing states untouched.
2. "How would you handle the machine running out of change coins?"
"Add a CoinInventory class tracking how many of each denomination the machine holds. calculateChange checks coin availability before committing. If the machine cannot make exact change, reject the purchase and tell the user to use exact change or cancel."
3. "How would you add a maintenance mode for restocking?"
"Add a MaintenanceState implementing VendingState. All user actions are rejected with a maintenance message. A new enterMaintenance() method transitions to this state. No existing state classes change, which is the whole point of the State pattern."
4. "How would you add purchase history or audit logging?"
"Add an Observer on VendingMachine that fires events on state transitions and successful purchases. A TransactionLogger listener records each event, keeping logging decoupled from the state machine."
Common Interview Mistakes
| Mistake | Why it's wrong | What to do instead |
|---|---|---|
| Using floating-point for money | Rounding errors (0.1 + 0.2 != 0.3) | Use integers (cents) for all prices and balances |
| Skipping the state diagram | Jumping into code without showing the state machine | Draw the state diagram first. Takes 30 seconds, shows understanding |
| Putting all logic in VendingMachine | 300-line class with switch statements | Delegate to state objects. Each state is small and focused |
| Forgetting to validate before mutating | Dispensing before checking stock or balance | Guard clauses first, state mutation last |
| Making state objects hold data | Balance stored in HasMoneyState | States define behavior only. Data lives on the context |
| Ignoring change calculation | Hand-waving "return change" | Show the greedy algorithm. Interviewers want to see it |
Interview pro tip: Start by drawing the state diagram on the whiteboard. Even before listing entities, show the three states and their transitions. This signals to the interviewer that you see the core abstraction.
Interview pro tip: When asked "what happens if the user does X?", trace through the state diagram. Point to the current state, follow the arrow, and explain the target state's behavior.
Test Your Understanding
Quick Recap
- Vending machines are state machine problems. Draw the state diagram before writing any code.
- Use the State pattern when the same action (insertCoin, selectProduct) behaves differently depending on which state the machine is in. Each state becomes its own class.
- Keep all mutable data (balance, inventory) on the context object. State classes define behavior only.
- Use integers (cents) for all money calculations. Floating-point arithmetic introduces rounding bugs that are hard to debug and embarrassing in interviews.
- Validate before mutating. Guard clauses at the top of each method reject invalid requests before any state changes occur.
- The greedy algorithm works for standard coin denominations. Mention dynamic programming as the general solution if the interviewer asks about arbitrary denominations.
- Extensibility comes from the pattern itself. Adding a new state or payment method requires new classes, not edits to existing ones. Say this explicitly in the interview.