Chapter 10 · Article 47 of 55

Designing a Vending Machine

Design an object-oriented system for a Vending Machine that handles product selection, payment processing, and product dispensing. The machine should support multiple payment me…

Article outline14 sections on this page

Problem Statement

Design an object-oriented system for a Vending Machine that handles product selection, payment processing, and product dispensing. The machine should support multiple payment methods (coins, notes, and cards), maintain an inventory of products across fixed slots, calculate and return correct change, and handle edge cases such as insufficient funds, out-of-stock items, and transaction cancellations.

Scenario: A user approaches the vending machine, views available products on the display, inserts money (coins or notes) or swipes a card, selects a product, receives the product along with any change due, and walks away. The system must also support administrative operations such as restocking products, collecting accumulated cash, and configuring pricing. The machine operates autonomously 24/7 and must gracefully handle hardware failures, power interruptions, and concurrent access attempts.

This is a classic low-level design (LLD) interview problem that tests your understanding of state machines, design patterns, encapsulation, and handling real-world constraints in an object-oriented manner.


Requirements

Functional Requirements

  1. Display Available Products - Show product name, price, and availability status for each slot on the machine's display panel.
  2. Accept Multiple Payment Types - Support coins (1¢, 5¢, 10¢, 25¢, 50¢, $1), notes ($1, $5, $10, $20), and card payments (credit/debit).
  3. Calculate Change - Compute the correct change to return after a purchase using available denominations in the coin/note reservoir.
  4. Dispense Product - Physically release the selected product from its slot upon successful payment.
  5. Handle Refunds - Allow users to cancel a transaction at any point before dispensing and return all inserted money.
  6. Inventory Management - Track quantity per slot, mark slots as empty, prevent selection of out-of-stock items.
  7. Admin Operations - Provide authenticated access for restocking products, collecting accumulated money, updating prices, and viewing sales reports.
  8. Transaction Logging - Record every transaction (successful or failed) for audit and reconciliation purposes.

Non-Functional Requirements

  1. Concurrent Access Handling - Ensure thread safety if multiple inputs arrive simultaneously (e.g., button presses during coin insertion). Only one transaction should be active at a time.
  2. Fault Tolerance - Recover gracefully from power failures, hardware jams, and sensor malfunctions. Persist transaction state to handle mid-transaction crashes.
  3. Accurate Money Handling - Use integer arithmetic (cents, not dollars) to avoid floating-point precision errors. Every cent must be accounted for.
  4. Responsiveness - The machine should respond to user input within 200ms for a smooth user experience.
  5. Maintainability - The design should be modular enough to swap hardware components (e.g., a new coin acceptor model) without rewriting business logic.

Constraints & Assumptions

  • The machine has a fixed number of product slots (e.g., 20 slots arranged in a grid of 4 rows × 5 columns).
  • Each slot holds a maximum of 10 units of a single product type.
  • The change reservoir has limited capacity - the machine may enter "exact change only" mode when change supply is low.
  • Supported denominations are predefined and fixed for a given deployment region.
  • Only one user transaction is active at any time (single-user physical machine).
  • Card payments are processed via an external payment gateway with potential network latency.
  • The machine has persistent storage (small flash memory) to survive power cycles.
  • Products do not expire within the scope of this design (can be added as an extension).
  • The admin panel is accessed via a physical key + PIN combination.

Key Entities & Relationships

EntityResponsibility
VendingMachineCentral controller; orchestrates all operations and holds references to subsystems
StateInterface for state-specific behavior (Idle, HasMoney, Dispensing, Refunding, OutOfStock)
ProductRepresents a product type with name, price, and category
SlotPhysical slot holding units of a product; tracks quantity and position (row, col)
InventoryManages all slots; provides lookup, availability checks, and restock operations
PaymentProcessorStrategy interface for handling different payment methods
CoinDispenserManages the change reservoir; calculates and dispenses change
DisplayObserver that renders current state, messages, and product info to the user
AdminPanelAuthenticated interface for administrative operations
TransactionRecords details of a single user interaction (items, amount, change, status)

Relationships:

  • VendingMachine has-a State (current), Inventory, PaymentProcessor, CoinDispenser, Display
  • Inventory has-many Slots; each Slot has-a Product and quantity
  • PaymentProcessor is a strategy interface with CashPayment and CardPayment implementations
  • Display observes VendingMachine state changes

ASCII Class Diagram

┌─────────────────────────────────────────────────────────────────┐
│                        VendingMachine                            │
├─────────────────────────────────────────────────────────────────┤
│ - currentState: State                                           │
│ - inventory: Inventory                                          │
│ - coinDispenser: CoinDispenser                                  │
│ - display: Display                                              │
│ - currentBalance: int (cents)                                   │
│ - selectedSlot: Slot                                            │
│ - transactionLog: List<Transaction>                             │
├─────────────────────────────────────────────────────────────────┤
│ + insertMoney(amount: int, type: PaymentType): void             │
│ + selectProduct(slotCode: String): void                         │
│ + cancelTransaction(): void                                     │
│ + dispenseProduct(): void                                       │
│ + setState(state: State): void                                  │
│ + getBalance(): int                                             │
└──────────────────────────┬──────────────────────────────────────┘
                           │ uses
              ┌────────────┼────────────────┐
              │            │                │
              ▼            ▼                ▼
┌──────────────────┐ ┌──────────┐ ┌────────────────────┐
│   <<interface>>  │ │Inventory │ │   CoinDispenser    │
│      State       │ ├──────────┤ ├────────────────────┤
├──────────────────┤ │-slots:   │ │-reservoir: Map<    │
│+insertMoney()    │ │ Map<Code,│ │  Denomination,int> │
│+selectProduct()  │ │ Slot>    │ ├────────────────────┤
│+cancel()         │ ├──────────┤ │+calculateChange()  │
│+dispense()       │ │+getSlot()│ │+dispenseChange()   │
└──────┬───────────┘ │+restock()│ │+canMakeChange()    │
       │             │+isEmpty()│ │+addCoins()         │
       │             └────┬─────┘ └────────────────────┘
       │                  │ has-many
  ┌────┼────┬─────┬───┐  ▼
  │    │    │     │   │  ┌──────────────────┐
  ▼    ▼    ▼     ▼   │  │      Slot        │
┌────┐┌────┐┌───┐┌──┐ │  ├──────────────────┤
│Idle││Has ││Dis││Out│ │  │-code: String     │
│    ││Mon ││pen││Of │ │  │-product: Product │
│    ││ey  ││se ││Stk│ │  │-quantity: int    │
└────┘└────┘└───┘└───┘ │  │-maxCapacity: int │
                       │  ├──────────────────┤
  ┌────────────────────┘  │+isAvailable()    │
  ▼                       │+dispenseOne()    │
┌──────────────────┐      │+restock(qty)     │
│   Refunding      │      └───────┬──────────┘
└──────────────────┘              │ has-a
                                  ▼
                         ┌──────────────────┐
                         │     Product      │
                         ├──────────────────┤
                         │-name: String     │
                         │-price: int       │
                         │-category: String │
                         └──────────────────┘

┌─────────────────────────┐
│  <<interface>>           │
│  PaymentProcessor       │
├─────────────────────────┤
│ +processPayment(): bool │
│ +refund(): bool         │
└────────┬────────────────┘
         │ implements
    ┌────┴─────┐
    ▼          ▼
┌────────┐ ┌──────────┐
│  Cash  │ │   Card   │
│Payment │ │ Payment  │
└────────┘ └──────────┘

State Machine Design

The vending machine's behavior is governed by a finite state machine. Each state encapsulates the valid operations and transitions available in that context.

                    ┌─────────────────────────────────┐
                    │                                 │
                    ▼                                 │
             ┌───────────┐                           │
             │           │                           │
     ┌──────►│   IDLE    │◄──────────────────┐      │
     │       │           │                   │      │
     │       └─────┬─────┘                   │      │
     │             │                         │      │
     │             │ insert money            │      │
     │             ▼                         │      │
     │       ┌───────────┐                   │      │
     │       │           │   cancel          │      │
     │       │ HAS_MONEY ├──────────►┌───────┴───┐  │
     │       │           │           │ REFUNDING  │──┘
     │       └─────┬─────┘           └───────────┘
     │             │
     │             │ select product
     │             │ (sufficient funds)
     │             ▼
     │       ┌───────────┐
     │       │           │
     │       │DISPENSING │
     │       │           │
     │       └─────┬─────┘
     │             │
     │             │ success + return change
     │             │
     └─────────────┘

     ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
     From ANY state, if all products are depleted:

             ┌───────────┐
             │OUT_OF_STOCK│
             │  (admin    │
             │  restock)  │──────► IDLE
             └───────────┘

State Transitions Table:

Current StateEventNext StateAction
IDLEInsert moneyHAS_MONEYAccept and accumulate balance
HAS_MONEYInsert more moneyHAS_MONEYAdd to balance
HAS_MONEYSelect product (funds ≥ price)DISPENSINGInitiate dispensing
HAS_MONEYSelect product (funds < price)HAS_MONEYDisplay "insufficient funds"
HAS_MONEYCancelREFUNDINGBegin refund process
REFUNDINGRefund completeIDLEReturn all inserted money
DISPENSINGProduct dispensedIDLEDispense product + change
DISPENSINGJam/failureREFUNDINGRefund and alert maintenance
ANYAll slots emptyOUT_OF_STOCKDisplay "out of stock" message
OUT_OF_STOCKAdmin restocksIDLEReset inventory, resume service

Design Decisions & Trade-offs

1. State Pattern vs If-Else for State Management

ApproachProsCons
State PatternOpen/Closed principle; adding new states doesn't modify existing code; each state is self-contained and testableMore classes; slight overhead for simple machines
If-Else / SwitchSimple for 2-3 states; less boilerplateBecomes unmaintainable as states grow; violates SRP; scattered transition logic

Decision: Use the State pattern. A vending machine has 5+ states with complex transitions. The State pattern makes each state's behavior explicit, prevents invalid operations (e.g., dispensing in IDLE state), and makes the system extensible for future states like MAINTENANCE or WARMING_UP.

2. Change Calculation Algorithm

ApproachProsCons
Greedy AlgorithmO(n) time; simple; works for standard coin systems (1, 5, 10, 25)May fail for non-canonical coin systems
Dynamic ProgrammingOptimal for any denomination set; minimizes coins returnedO(amount × denominations) time and space; overkill for standard systems

Decision: Use the greedy algorithm. Standard currency denominations are canonical (greedy always produces optimal results). Add a fallback check: if greedy fails to make exact change, enter "exact change only" mode rather than attempting DP.

3. Inventory Tracking: Per-Slot vs Centralized

ApproachProsCons
Per-SlotDirect mapping to physical reality; simple dispensing logicSame product in multiple slots requires coordination
CentralizedAggregate view of product availability; easier reportingMust still map to physical slots for dispensing; added indirection

Decision: Use per-slot tracking as the source of truth (mirrors physical hardware), with a centralized Inventory facade that provides aggregate queries. This gives the best of both worlds: physical accuracy and logical convenience.

4. Payment Abstraction

ApproachProsCons
Unified interfaceClean abstraction; easy to add new payment typesMust handle fundamentally different flows (async card vs sync cash)
Separate handlingEach payment type optimized independentlyCode duplication; harder to combine payment methods

Decision: Use the Strategy pattern with a common PaymentProcessor interface. Cash and card implementations handle their specific flows internally. For mixed payments (partial cash + card), introduce a CompositePaymentProcessor that delegates to multiple strategies.


Design Patterns Used

1. State Pattern (Core)

The machine's behavior changes dramatically based on its current state. Each state (Idle, HasMoney, Dispensing, Refunding, OutOfStock) implements a common State interface. The VendingMachine delegates all user actions to its current state object, which handles the action and triggers transitions.

2. Strategy Pattern (Payment Processing)

Different payment methods (CashPayment, CardPayment, MobilePayment) implement a common PaymentProcessor interface. The machine selects the appropriate strategy based on the payment type detected. New payment methods can be added without modifying existing code.

3. Factory Pattern (Product Creation)

A ProductFactory creates Product instances from configuration data during initialization or restocking. This centralizes validation (price > 0, name not empty) and can be extended to create specialized product types (refrigerated, fragile).

4. Observer Pattern (Display Updates)

The Display subscribes to state changes in the VendingMachine. Whenever the state transitions, balance changes, or an error occurs, the Display is notified and updates its output. This decouples the UI from business logic entirely.

5. Singleton Pattern (Machine Instance)

In a single physical machine context, the VendingMachine is a singleton. This prevents accidental creation of multiple instances that could corrupt shared state (inventory, balance). Access is controlled through a getInstance() method with thread-safe lazy initialization.


Edge Cases

1. Insufficient Change Available

The machine cannot make change for the user's payment. Handling: Before accepting large denominations, check if change can be made for the most expensive product. If not, enter "Exact Change Only" mode and display a warning. Reject bills that would create an unmakeable change scenario.

2. Product Stuck (Dispensing Failure)

The motor turns but the product doesn't fall (sensor doesn't detect it). Handling: Retry dispensing once. If still stuck, transition to REFUNDING state, return the user's money, mark the slot as potentially jammed, and alert maintenance via the admin panel.

3. Power Failure Mid-Transaction

Power cuts while the user has inserted money but hasn't received a product. Handling: Persist transaction state to non-volatile storage after each money insertion. On power restore, check for incomplete transactions and either complete them or enter refund mode.

4. Exact Change Only Mode

The change reservoir is depleted below a threshold. Handling: Calculate the minimum change reservoir needed to handle worst-case scenarios. When below threshold, switch display to "Exact Change Only" and only accept payments that match product prices exactly (or card payments).

5. Expired Products

Products past their sell-by date remain in slots. Handling: Store expiry dates per slot. During product selection, check expiry before dispensing. Expired products are marked unavailable and flagged for admin removal. This is an extension point.

6. Concurrent Button Presses

User rapidly presses multiple buttons or inserts coins while selecting. Handling: Use a mutex/lock on the state machine. All inputs are queued and processed sequentially. The current state determines which inputs are valid; invalid inputs are ignored with appropriate feedback.

7. Card Payment Timeout

Network is slow or unavailable during card processing. Handling: Set a timeout (e.g., 30 seconds) for card authorization. If exceeded, cancel the card transaction, notify the user, and suggest cash payment. No money is deducted from the card unless authorization is confirmed.


Extensibility Points

  1. New Payment Methods - Implement the PaymentProcessor interface for mobile wallets (Apple Pay, Google Pay), QR code payments, or cryptocurrency. No changes to core logic required.

  2. Loyalty Programs - Add a LoyaltyManager that observes transactions. After N purchases, apply a discount or dispense a free item. Integrates via the Observer pattern on the transaction log.

  3. Remote Monitoring - Add a TelemetryService that reports inventory levels, sales data, error rates, and machine health to a central dashboard. Operators can monitor fleets of machines and dispatch restocking crews proactively.

  4. Dynamic Pricing - Introduce a PricingStrategy that adjusts prices based on time of day, demand, inventory levels, or promotions. The Slot delegates pricing to this strategy rather than storing a fixed price.

  5. Touchscreen UI - Replace the physical button + LED display with a touchscreen. The Display interface abstraction means the business logic is unchanged; only the rendering implementation swaps out.

  6. Multi-Language Support - The Display can accept locale settings and render messages in the user's preferred language, driven by a language selection on the UI.


Interview Follow-ups

Q1: How would you handle a scenario where the machine needs to support combo purchases (buy 2 items in one transaction)?

Answer: Extend the HAS_MONEY state to allow multiple product selections before dispensing. Introduce a Cart object that accumulates selected items and their total price. The DISPENSING state iterates through the cart, dispensing each item sequentially. If any item fails to dispense, refund only that item's price and dispense the rest. The state machine gains a new transition: HAS_MONEY → (select another product) → HAS_MONEY, and HAS_MONEY → (confirm purchase) → DISPENSING.

Q2: How would you implement the change-making algorithm to handle the case where greedy fails?

Answer: For standard denominations (1, 5, 10, 25, 50, 100 cents), greedy always works optimally. However, if the machine operates in a region with non-canonical denominations, implement a fallback: first attempt greedy, then if it fails to produce exact change using available coins, use dynamic programming with the constraint of available coin counts. If neither can produce exact change, enter "exact change only" mode. The key insight is tracking not just denominations but actual available quantities of each denomination in the reservoir.

Q3: How would you ensure data consistency if the machine crashes between debiting the user's card and dispensing the product?

Answer: Use a write-ahead log (WAL) pattern. Before initiating card charge, write the intent to persistent storage. After successful charge, update the log. After successful dispensing, mark the transaction complete. On restart, replay the log: if charged but not dispensed, either dispense the product or initiate a card refund via the payment gateway. This gives exactly-once semantics for the user. Include a unique transaction ID to make refund operations idempotent.

Q4: The machine needs to support "buy one get one free" promotions. How would you design this?

Hint 1: Consider a PromotionEngine that evaluates rules against the current cart before calculating the final price.

Hint 2: Promotions could be modeled as decorators on the pricing calculation, or as a chain-of-responsibility where each promotion rule gets a chance to modify the transaction total.

Q5: How would you design the system to support a fleet of 10,000 vending machines reporting to a central server?

Hint 1: Think about an event-driven architecture where each machine publishes events (sale, restock, error) to a message queue, and the central server consumes and aggregates them.

Hint 2: Consider eventual consistency - the central server doesn't need real-time accuracy. Machines operate autonomously and sync periodically. Use machine-local decision-making for dispensing and change, with server-side analytics for fleet management.


Counter Questions to Ask Interviewer

Before diving into the design, clarify scope and constraints with these questions:

  1. "Is this a single physical machine or a fleet management system?" - Determines whether you need networking, central coordination, or just local OOP design.

  2. "What payment methods must be supported? Cash only, or also card/mobile?" - Affects complexity of the payment subsystem and whether you need external service integration.

  3. "Should the machine support multiple product selections per transaction (combo purchases)?" - Changes the state machine significantly if yes.

  4. "What's the expected product catalog size? Fixed slots or configurable?" - Determines whether you need a flexible inventory system or a simple fixed array.

  5. "Do we need to handle concurrent users (e.g., a machine with two dispensing bays)?" - Determines thread-safety requirements and whether the singleton assumption holds.

  6. "Is persistence required across power failures?" - Determines whether you need a WAL, database, or can assume volatile state.

  7. "Should the design focus on the class structure (LLD) or the distributed system aspects (HLD)?" - Clarifies depth vs breadth expectations.


References

  1. "Design Patterns: Elements of Reusable Object-Oriented Software" - Gamma, Helm, Johnson, Vlissides (Gang of Four). Chapters on State, Strategy, and Observer patterns.
  2. "Head First Design Patterns" - Freeman & Robson. Accessible treatment of State and Strategy patterns with real-world examples.
  3. "Clean Code" - Robert C. Martin. Principles of SRP and OCP that motivate the State pattern choice.
  4. "Refactoring: Improving the Design of Existing Code" - Martin Fowler. Replace Conditional with Polymorphism refactoring (directly applicable to State pattern).
  5. IEEE Std 610.12 - Software engineering terminology for state machine definitions.
  6. "Object-Oriented Analysis and Design with Applications" - Grady Booch. Foundational OO modeling techniques used in entity identification.
  7. Greedy Algorithms for Change-Making - Canonical coin systems and when greedy is optimal (Magazine, Nemhauser, Trotter, 1975).

This problem tests state machine design, pattern application, and real-world constraint handling. Focus on clean state transitions, proper encapsulation of payment logic, and graceful degradation under failure conditions. The interviewer is evaluating your ability to model physical-world constraints in software while keeping the design extensible.