Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Design Vending Machine
1. Requirements
Functional Requirements
| Feature | Description |
|---|---|
| Display Products | Show available products with prices |
| Accept Money | Accept coins and bills |
| Select Product | User selects product by code |
| Dispense Product | Deliver product if sufficient payment |
| Return Change | Return excess money |
| Cancel Transaction | Return all inserted money |
| Admin Operations | Refill products, collect money |
Constraints
- Support multiple payment methods (coins, bills, card)
- Handle exact change scenarios
- Track inventory per product slot
- Thread-safe operations
2. State Machine
The vending machine is a perfect example of the State Pattern. The machine’s behavior changes based on its current state.3. Core Entities
4. State Pattern Implementation
4.1 State Interface
4.2 Idle State
4.3 Has Money State
4.4 Dispensing State
5. Vending Machine Class
6. Cash Inventory & Change Calculation
7. Strategy Pattern for Payment
8. Complete Usage Example
9. Class Diagram
10. Key Takeaways
| Concept | Implementation |
|---|---|
| State Pattern | Machine behavior changes based on current state |
| Strategy Pattern | Multiple payment methods without changing core logic |
| Thread Safety | Lock protects concurrent operations |
| Greedy Algorithm | Optimal change calculation |
| SRP | Separate classes for state, payment, inventory |
11. Extensions
How to handle 'exact change only' scenarios?
How to handle 'exact change only' scenarios?
- Before each transaction, check
can_make_change()for potential change amounts - Display warning if machine cannot make change
- Reject high-denomination bills when low on change
- Allow admin to set minimum change reserve
How to add a display/UI layer?
How to add a display/UI layer?
- Create
Displayinterface with methods likeshowMessage(),showProducts() - Inject display into VendingMachine
- Call display methods at appropriate points in state transitions
- Support different display types (LCD, LED, touchscreen) via abstraction
Interview Deep-Dive Questions
Q1: Compare the State pattern used in this vending machine vs. a simple enum-based state check. When would you actually prefer the enum approach?
Q1: Compare the State pattern used in this vending machine vs. a simple enum-based state check. When would you actually prefer the enum approach?
- The State pattern shines when each state has substantially different behavior for the same operation. In the vending machine,
insert_money()does completely different things in IdleState (create a balance and transition), HasMoneyState (accumulate balance), and DispensingState (reject with a message). Encoding all of this inif state == IDLE: ... elif state == HAS_MONEY: ...blocks means every method has a growing switch statement, and adding a new state (e.g.,OutOfOrder) means modifying every single method. - However, the enum approach is genuinely preferable when: (a) the state machine is small (2-3 states), (b) the behavior differences are trivial (e.g., just a boolean check), or (c) the states need to be serialized and persisted frequently. Serializing an enum to a database is trivial; serializing a State object requires additional infrastructure.
- A practical hybrid is common in production: use an enum for persistence and external APIs (the state stored in the DB is always
"idle","has_money","dispensing"), but internally in the running process, map that enum to a State object that encapsulates behavior. This gives you the best of both: clean behavior dispatch in-process and simple persistence. - The State pattern also introduces a subtle coupling: each state class holds a reference to the machine (
self.machine), which creates a bidirectional dependency. In the current code, the state constructor takes the machine as a parameter, meaning states cannot be reused across machines without rebuilding them. This is fine for a vending machine but becomes a concern in systems with many concurrent state machines.
- If you needed to persist the vending machine’s state across power outages, how would you serialize and deserialize the current State pattern implementation?
- In the current design, each state transition creates a new state object (e.g.,
HasMoneyState(self.machine)). What is the garbage collection impact if the machine processes 10,000 transactions per day, and how would you optimize it?
Q2: The payment handling uses a Strategy pattern with CashPayment, CardPayment, and MobilePayment. But the current state machine only models the cash flow. How would you integrate card and mobile payments into the state transitions?
Q2: The payment handling uses a Strategy pattern with CashPayment, CardPayment, and MobilePayment. But the current state machine only models the cash flow. How would you integrate card and mobile payments into the state transitions?
- The current state machine assumes a sequential cash flow: insert money incrementally, then select product. Card and mobile payments break this model because the full payment happens atomically after product selection, not incrementally before it.
- You need a fork in the state machine. From IdleState, there should be two paths: (1)
insert_money-> HasMoneyState (cash flow, as-is), and (2)select_product-> ProductSelectedState (card/mobile flow). In the card/mobile path, the user selects a product first, then pays the exact amount in one step. This means IdleState’sselect_product()should no longer blindly return “Please insert money first” — it should check if a non-cash payment method is available. - You could introduce a
ProductSelectedStatethat sits between selection and dispensing, where the machine waits for card tap or mobile confirmation. This state has a timeout (e.g., 30 seconds) after which it returns to Idle. - The Strategy pattern and State pattern interact here: the current state determines which payment strategies are valid. In HasMoneyState, only CashPayment applies. In ProductSelectedState, only CardPayment and MobilePayment apply. The state can hold a reference to the valid
PaymentStrategyand delegate to it. - A real-world concern: card payments require network connectivity. The machine needs a way to gracefully degrade — if the card reader or network is down, only the cash flow should be available. This means the state machine’s available transitions are dynamic based on hardware health status, not just hardcoded.
CardPayment.process_payment() inside HasMoneyState.” This misses that the cash flow and card flow have fundamentally different state sequences, and jamming card logic into the cash-oriented states creates a conceptual mess.Follow-ups:- Card payments can fail due to network timeouts. How does the state machine handle a payment that is in-flight for 10 seconds — do you block the machine, show a spinner, or allow cancellation?
- If the card payment succeeds but the dispenser jams, how do you initiate an automatic refund, and which class is responsible for that coordination?
Q3: The inventory system uses a simple Slot with a quantity counter. How would you redesign it to support expiration dates, dynamic pricing, and product recommendations?
Q3: The inventory system uses a simple Slot with a quantity counter. How would you redesign it to support expiration dates, dynamic pricing, and product recommendations?
- The current
Slotis essentially a dumb container: it knows its product, quantity, and max capacity. For a production vending machine, each individual item in a slot could have different expiration dates (e.g., front items expire sooner). You would changeSlotfrom tracking aquantity: intto maintaining aList[ProductItem]where eachProductItemhas its ownexpiration_date. Dispensing always takes the item with the nearest expiration (FIFO or earliest-expiry-first). - Dynamic pricing connects the
PricingStrategyconcept to inventory state. When a product is near expiration, you apply a discount; when inventory is low and demand is high, you increase the price. This requires theSlot(or anInventoryManager) to expose signals likedays_until_earliest_expiry()andstock_percentage(), which the pricing strategy consumes. - Product recommendations require the machine to track purchase history. A simple approach: maintain a co-purchase matrix — “people who bought A1 also bought B2” — and display a suggestion on screen after dispensing. This is an Observer pattern application: the
DispenseEventtriggers aRecommendationObserverthat queries the matrix and callsscreen.display_suggestion(). - An important real-world constraint is that vending machine hardware is resource-constrained. You cannot run a full recommendation engine on-device. The pattern is: sync a precomputed recommendation table from a central server during off-peak hours, and look it up locally. This keeps the on-device logic simple while enabling sophisticated recommendations.
- If the machine detects that all items in a slot expire within 24 hours, should it automatically apply a 50% discount, block the slot, or alert the operator? How would you make this configurable?
- How would you design an A/B test framework for testing different recommendation strategies on a fleet of 500 vending machines?
Q4: The change-making algorithm uses a greedy approach. Show me a specific input where this fails, and walk through how you'd fix it.
Q4: The change-making algorithm uses a greedy approach. Show me a specific input where this fails, and walk through how you'd fix it.
- The current coins are PENNY (0.01), NICKEL (0.05), DIME (0.10), QUARTER (0.25), DOLLAR (1.00). For these denominations, greedy always works because they form a canonical coin system. But let us say a machine in a different market has denominations of 1, 15, and 25 cents. If a customer needs 30 cents in change, greedy picks 25 + 1 + 1 + 1 + 1 + 1 = six coins. The optimal answer is 15 + 15 = two coins.
- The fix is dynamic programming. Build a table
dp[0..amount]wheredp[i]is the minimum number of coins to make amounti. Initializedp[0] = 0and all others to infinity. For each amount from 1 to target, try each coin denomination and takedp[i] = min(dp[i], dp[i - coin] + 1). Then backtrack to find which coins were used. - But there is a subtlety the greedy-vs-DP framing misses: the machine has limited coin inventory. Even with standard denominations, greedy can fail not because it picks the wrong coins but because it runs out. If the machine has zero quarters but 20 dimes, greedy for 50 cents would try 2 quarters (fail), then what? The current
get_changemethod handles this by checkingself.coins[coin] > 0in the while loop, which naturally falls through to smaller coins. But for non-canonical denominations with limited supply, you need DP with inventory constraints, which is essentially the bounded knapsack problem. - In the current code, there is a safety net: if
remaining > 0.001after the greedy pass, it restores all coins and returns an empty list. This is correct behavior (refuse to make change rather than short-change the customer), but the user experience is terrible — the customer inserted money but cannot buy anything. A better approach: before accepting money, checkcan_make_change()for the maximum possible change amount and display “EXACT CHANGE ONLY” proactively.
- The
get_changemethod usesDecimal("0.001")as a floating-point error tolerance. What specific bug could this mask, and how would you eliminate the tolerance entirely? - If the machine is low on change, how would you implement a proactive “EXACT CHANGE ONLY” mode that activates before any customer is affected?
Q5: The DispensingState's cancel() method returns Decimal('0') -- meaning the user cannot cancel during dispensing. Is this the right design choice?
Q5: The DispensingState's cancel() method returns Decimal('0') -- meaning the user cannot cancel during dispensing. Is this the right design choice?
- It is the correct choice for physical dispensing because the mechanical process is essentially irreversible once started. The motor has already pushed the product to the delivery slot. You cannot un-dispense a bag of chips. This is different from a software transaction where you can roll back.
- However, there is a timing gap between “product selected, transitioning to DispensingState” and “physical dispenser motor engaged.” In this gap, cancellation should theoretically still be possible. The current design does not model this sub-state. A more precise implementation would have
DispensingPendingState(cancellable) andDispensingActiveState(not cancellable), with the transition triggered by a hardware callback from the dispenser motor. - The broader design question is: what should happen if the dispensing mechanism jams or fails? The current
dispense()method handles this by refunding the full amount ifslot.dispense()returns None. But there is a race condition: the slot’s quantity was already decremented bydispense()before the physical mechanism might fail. You need to separate the logical inventory decrement from the physical dispense confirmation. - In real vending machines, there are sensors at the delivery chute that confirm the product actually fell. The sequence is: (1) command the motor, (2) wait for the delivery sensor, (3) if confirmed within timeout, decrement inventory and complete; if not confirmed, retry once, then refund and mark the slot as potentially jammed.
- If the product gets stuck (detected by a delivery chute sensor timeout), should the machine refund the customer, retry the dispense, or move to an out-of-order state? Walk me through the decision tree.
- How would you design the
Slot.dispense()method to separate the logical inventory reservation from the physical delivery confirmation?
Q6: How would you add extensibility for a new product category like hot beverages (coffee, tea) that require preparation time?
Q6: How would you add extensibility for a new product category like hot beverages (coffee, tea) that require preparation time?
- Hot beverages break the current synchronous dispensing model. A bag of chips dispenses in 1-2 seconds. A coffee takes 30-60 seconds to brew. The DispensingState needs to become asynchronous — it cannot block the entire machine state for 60 seconds.
- You would introduce a
ProductDispenserinterface with anasync_dispense()method.SimpleDispenser(for snacks) calls the motor and returns immediately.BrewingDispenser(for coffee) starts the brewing process and returns a future/callback. The DispensingState transitions based on the dispenser callback, not a synchronous return value. - The state machine needs a new state:
PreparingState. The flow becomes:HasMoney -> ProductSelected -> PreparingState -> DispensingState -> Idle. In PreparingState, the screen shows a progress indicator, and the machine continues monitoring for hardware events (cup placed, brew complete, error). - This also introduces new failure modes: what if the coffee machine runs out of water or milk mid-brew? You need a
PartialDispenseErrorconcept where the machine has committed resources (water, coffee grounds) but cannot complete. The refund policy here is different from a snack dispenser jam — you might refund the money but you have already used consumables. The machine needs to track consumable inventory separately from product inventory. - For the class structure, each
Slotwould hold a reference to itsProductDispenserimplementation. This follows the Strategy pattern at the slot level, making the vending machine agnostic to how each product is actually dispensed.
- If two hot beverages are ordered back-to-back, should the machine start brewing the second while the first is being picked up, or enforce a sequential flow? What are the trade-offs?
- How would you model consumable ingredients (water, milk, coffee beans) that are shared across multiple product types, and how does this affect the
is_empty()check?
Q7: The VendingMachine class uses a single threading.Lock for all operations. What are the performance implications, and how would you improve it?
Q7: The VendingMachine class uses a single threading.Lock for all operations. What are the performance implications, and how would you improve it?
- A single lock means all operations are fully serialized: if one customer is in the middle of inserting a coin, another thread (e.g., a background inventory check, a network health ping, or an admin refill operation) is completely blocked. For a physical vending machine with one customer at a time this is acceptable, but for a virtual vending machine (think: an API-based system managing multiple kiosk screens on the same backend), this is a bottleneck.
- The improvement is fine-grained locking. The cash inventory (
CashInventory) should have its own lock, separate from the slot inventory lock. The state machine transitions should be protected by a state lock. This allows, for example, an admin to check inventory levels without blocking a customer mid-transaction. - Read-write locks (
threading.RWLockor equivalent) are another improvement:get_available_products()is a read-only operation and should not block other readers. Only write operations (dispensing, refilling) need exclusive access. - But there is a deeper issue: the
_statefield andcurrent_balanceare logically coupled. You cannot safely update one without considering the other. Fine-grained locking introduces the risk of deadlocks if locks are acquired in inconsistent order. The disciplined approach is to define a lock ordering protocol: always acquire the state lock before the inventory lock, never the reverse. - In production vending machine firmware, the common pattern is an event loop (single-threaded) with a message queue. Hardware events (coin inserted, button pressed, dispenser sensor) are enqueued as messages, and the main loop processes them sequentially. This eliminates locking entirely through the actor model. The current threading approach is the OOP-textbook solution; the event loop is the production-firmware solution.
asyncio instead of threads.” This confuses I/O concurrency with CPU concurrency and does not address the fundamental problem of protecting shared mutable state.Follow-ups:- If you moved this to an event loop architecture, how would you handle a long-running hardware operation (e.g., the dispenser motor takes 3 seconds) without blocking the event loop?
- The
dispense_change()method is called outside the lock inDispensingState.dispense(). Is this a bug? What could go wrong?
Q8: A customer inserts $10 but only buys a $1.50 soda. Walk me through the exact change-making process and identify every failure point.
Q8: A customer inserts $10 but only buys a $1.50 soda. Walk me through the exact change-making process and identify every failure point.
- The customer inserts a 10.00. They select product A1 (Coca-Cola, 10.00 - 8.50
. Nowdispense_change($8.50)` is called. - The greedy algorithm in
get_change()tries: DOLLAR coins first. If the machine has 8 dollar coins, it dispenses 8 x 8.00, then 1 x QUARTER = 8.50. That is 10 coins total for $8.50 in change. This is a lot of coins clinking out, which is already a bad user experience. - Failure point 1: The machine might not have enough coins. If it has only 3 dollar coins and 5 quarters, it can make 1.25 = 8.50. The
get_changemethod returns an empty list, and… then what? The current code inDispensingState.dispense()does not check whether change was successfully dispensed. The product is already given, money is deducted, and the customer is simply shorted their change. This is a bug. - Failure point 2: The
can_make_change()check should happen before dispensing, not after. The correct flow is: (1) check if change can be made forbalance - price, (2) only then dispense the product, (3) then dispense the change. The current code does it backwards. - Failure point 3: Bills cannot be used as change in the current model. The
get_change()method only iterates overCoinvalues, notBillvalues. So even if the machine is full of $1 bills, it cannot use them for change. This is a significant gap. - Failure point 4: The $10 bill the customer just inserted is now in the machine’s bill inventory, but bills are never used for change. The machine’s coin supply is being depleted without replenishment. Over time, the machine accumulates bills and runs out of coins — a classic vending machine operator headache.
- How would you redesign the system to reject a 1.50 purchase upfront, before the customer inserts it?
- Vending machine operators typically “seed” the machine with a change float (e.g., $20 in quarters). How would you model this in the design, and when should the machine automatically switch to “exact change only” mode?