Low-Level Design: Designing a Vending Machine using the State Pattern
Design Challenge: The Fragile Vending Machine
Designing a Vending Machine is a classic low-level design interview problem that tests state-driven logic.
🎯 System Scenario & Requirements
System Goal
Designing a Vending Machine is a classic low-level design interview problem that tests state-driven logic.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: Designing a Vending Machine is a classic low-level design interview problem that tests state-driven logic.
- By using the GoF State Pattern, we avoid nested conditional blocks and build a modular, thread-safe system in Java Spring Boot.
- Imagine you are tasked with developing the control software for a next-generation office vending machine.
Constraints & Assumptions
- Every architectural choice has trade-offs: Class Proliferation: The State Pattern requires creating a new class for every single state.
- For small machines with only 2 or 3 states, this is often over-engineering.
⚡ Execution Stages & Decision Trace
Stage 1 — Scope: Use Cases and System Actors
Before drawing class structures, we must establish the system boundary and identify the core functional requirements.
Customer: The primary user who inserts money, selects products, requests refunds, and collects snacks and change.
Deep Dive
Scenario context
Designing a Vending Machine is a classic low-level design interview problem that tests state-driven logic. By using the GoF State Pattern, we avoid nested conditional blocks and build a modular, thread-safe system in Java Spring Boot.
Imagine you are tasked with developing the control software for a next-generation office vending machine. The machine must accept coins of different denominations, track a dynami…
This walkthrough focuses on Design Challenge: The Fragile Vending Machine.
Before making a design choice, clarify:
- Imagine you are tasked with developing the control software for a next-generation office vending machine.
- The machine must accept coins of different denominations, track a dynamic inventory of snacks, validate user selections, dispense products, and return change.
- A naive approach might rely on nested conditional checks to manage the machine's state: This code is fragile.
Concepts in play: low-level-design-systems, design-principles-solid, jvm-internals-and-garbage-collection, api-gateway
Stage 1 — Scope: Use Cases and System Actors
- Before drawing class structures, we must establish the system boundary and identify the core functional requirements.
- Customer: The primary user who inserts money, selects products, requests refunds, and collects snacks and change.
- Maintenance Technician: An administrative user who refills inventory, collects accumulated cash, and audits machine health.
- Core Functional Requirements (Use Cases) Insert Money: The Customer inserts coins of recognized denominations (Nickel, Dime, Quarter, Dollar).
Observable outcome: Core Functional Requirements (Use Cases) Insert Money: The Customer inserts coins of recognized denominations (Nickel, Dime, Quarter, Dollar).
Stage 2 — Architectural Blueprint: Class Diagram
- The class diagram below maps the relationships between the main components of our vending machine design, detailing variables, methods, and interface dependencies.
- This class diagram illustrates the implementation of the State Pattern.
- The VendingMachine maintains a reference to a VendingState interface and delegates all actions to it.
- The four concrete state implementations— IdleState , HasMoneyState , DispensingState , and OutOfStockState —override these actions to execute state-specific logic and trigger transitions by calling VendingMachine.setCur…
classDiagram
VendingMachine "1" *-- "1" VendingState : delegates to
VendingMachine "1" *-- "1" Inventory : manages
VendingState <|.. IdleState : implements
VendingState <|.. HasMoneyState : implements
VendingState <|.. DispensingState : implements
VendingState <|.. OutOfStockState : implements
class VendingState {
<>
+insertCoin(VendingMachine machine, Coin coin) void
+selectProduct(VendingMachine machine, String slotId) void
+dispense(VendingMachine machine) void
+refund(VendingMachine machine) void
}
class VendingMachine {
-VendingState currentState
-Inventory inventory
-int balance
+setCurrentState(VendingState state) void
+insertCoin(Coin coin) void
+selectProduct(String slotId) void
+dispense() void
+refund() void
}
Observable outcome: The four concrete state implementations— IdleState , HasMoneyState , DispensingState , and OutOfStockState —override these actions to execute state-specific logic and trigger transitions by calling VendingMachine.setCur…
Stage 3 — Core Mechanics: Mapping the OOP Pillars
- Our design leverages the four core pillars of Object-Oriented Programming to ensure structural integrity: Abstraction: The VendingState interface defines a boundary.
- It hides state-specific transition rules from the VendingMachine and the client.
- The client calls generic methods like insertCoin without knowing how the machine transitions internally.
- Encapsulation: The VendingMachine encapsulates its internal variables—such as balance , selectedSlot , and inventory —restricting direct manipulation.
Observable outcome: Encapsulation: The VendingMachine encapsulates its internal variables—such as balance , selectedSlot , and inventory —restricting direct manipulation.
Stage 4 — Deep Dive: SOLID Walkthrough and Concurrency
- Let us walk through how our architecture implements SOLID design principles and handles concurrency in production.
- The Internals of State-Driven Execution Our vending machine design enforces multiple SOLID principles to maintain high codebase maintainability: Single Responsibility Principle (SRP): Each concrete state class has a sin…
- For example, IdleState only handles the initial coin insertion, delegating product selection warnings back to the client.
- Open-Closed Principle (OCP): Introducing a new state (e.g., MaintenanceState or CardAuthorizationState ) only requires writing a new class that implements VendingState and updating the transitions.
Observable outcome: Open-Closed Principle (OCP): Introducing a new state (e.g., MaintenanceState or CardAuthorizationState ) only requires writing a new class that implements VendingState and updating the transitions.
Stage 5 — Visualizing State Transitions: The Vending Flow
- To understand how the machine behaves as the user interacts with it, we map the lifecycle of a transaction.
- The diagram below shows the paths between states based on trigger actions.
- This state transition flowchart maps the operational phases of the vending machine.
- The system begins in the Idle state.
flowchart TD
Idle([Idle State]) -->|insertCoin| HasMoney([Has Money State])
HasMoney -->|insertCoin| HasMoney
HasMoney -->|selectProduct - balance ok| Dispensing([Dispensing State])
HasMoney -->|refund / cancel| Idle
Dispensing -->|dispense completed| Idle
Dispensing -->|dispense - empty| OutOfStock([Out of Stock State])
OutOfStock -->|refund| Idle
Observable outcome: The system begins in the Idle state.
Stage 6 — Real-World Endpoint: Spring REST API
- To expose our LLD simulation to the client, we implement a Spring Boot @RestController mapping endpoints to the VendingMachine singleton.
- This controller handles coin insertions, product selections, and cancel requests, executing state transitions concurrently inside Spring container threads.
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/vending")
public class VendingMachineController {
private final VendingMachine machine = new VendingMachine();
@GetMapping("/status")
public VendingStatus getStatus() {
return new VendingStatus(machine.getBalance(), machine.getSelectedSlot());
}
@PostMapping("/insert")
public String insertCoin(@RequestParam Coin coin) {
machine.insertCoin(coin);
return "Balance updated: " + machine.getBalance();
}
@PostMapping("/select")
public String selectProduct(@RequestParam String slotId) {
machine.selectProduct(slotId);
return "Selection processed.";
}
@PostMapping("/refund")
public String refund() {
machine.refund();
return "Refund complete.";
}
}
class VendingStatus {
private final int balance;
private final String selectedSlot;
public VendingStatus(int balance, String selectedSlot) {
this.balance = balance;
this.selectedSlot = selectedSlot;
}
public int getBalance() { return balance; }
public String getSelectedSlot() { return selectedSlot; }
}
Observable outcome: This controller handles coin insertions, product selections, and cancel requests, executing state transitions concurrently inside Spring container threads.
End-to-end outcome
After working through the stages, you should be able to explain:
- When designing state-driven LLD systems: 1.
- Keep Context Synchronized: Always declare context methods that alter mutable variables (like insertCoin , selectProduct ) as synchronized to ensure thread-safety in multi-threaded container environments.
- State Pattern: Encapsulates state-specific logic in dedicated classes, avoiding nested if-else structures.
- OOP Abstraction: Hides transition details completely behind interface contracts.
Alternatives considered
The source focuses on one primary design. A second architecture is not compared, so no alternative is invented here.
Failure and degraded modes
Trade-offs and Failure Modes: State Pattern Overhead
- Detection: Every architectural choice has trade-offs: Class Proliferation: The State Pattern requires creating a new class for every single state.
- User or operator impact: Every architectural choice has trade-offs: Class Proliferation: The State Pattern requires creating a new class for every single state.
