Skip to main content
Articles/Java/lld-vending-machine-state-pattern
Series: Low-Level Design Patterns•Part 5 of 5
Scenario Article

Low-Level Design: Designing a Vending Machine using the State Pattern

Design Challenge: The Fragile Vending Machine

Abstract Algorithms•Jun 18, 2026
💡 Executive Summary

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

1

Stage 1 — Scope: Use Cases and System Actors

Action / Decision:

Before drawing class structures, we must establish the system boundary and identify the core functional requirements.

Component Behavior:

Customer: The primary user who inserts money, selects products, requests refunds, and collects snacks and change.

Observable Output: Maintenance Technician: An administrative user who refills inventory, collects accumulated cash, and audits machine health.
Read Full Stage in Deep Dive ↓
📖

Deep Dive

Detailed Architectural Analysis

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.

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit