System Design
Object-Oriented Design
Vending Machine

Design Example: Vending Machine

The Vending Machine is a classic OOD problem that perfectly illustrates the State Design Pattern. It manages a complex lifecycle where the machine's behavior changes based on its current state.


Step 1: Requirements Gathering

Core Use Cases:

  • User inserts coins/cash.
  • User selects a product.
  • Machine dispenses the product if the balance is sufficient.
  • Machine returns change if necessary.
  • Admin can top up products or collect cash.

States:

  1. Idle: Waiting for money.
  2. HasMoney: Money inserted, waiting for selection.
  3. Dispensing: Product selected and being delivered.
  4. OutOfStock: No products available.

Step 2: Identify Core Objects

  • VendingMachine (Context): The main class that delegates behavior to state objects.
  • State (Interface): Defines the interface for all possible states.
  • Product: Represents an item with a price and quantity.
  • Inventory: Manages the stock of products.

Step 3: Design Class Diagram (State Pattern)

The State Pattern allows the VendingMachine to change its behavior at runtime by switching its internal state object.


Step 4: Implementation in TypeScript

// 1. State Interface
interface State {
  insertMoney(amount: number): void;
  selectProduct(productId: string): void;
  dispense(): void;
}
 
// 2. Concrete States
class IdleState implements State {
  constructor(private machine: VendingMachine) {}
 
  insertMoney(amount: number) {
    console.log(`Money inserted: $${amount}`);
    this.machine.addBalance(amount);
    this.machine.setState(this.machine.getHasMoneyState());
  }
 
  selectProduct(id: string) { console.log("Insert money first!"); }
  dispense() { console.log("Insert money first!"); }
}
 
class HasMoneyState implements State {
  constructor(private machine: VendingMachine) {}
 
  insertMoney(amount: number) {
    this.machine.addBalance(amount);
  }
 
  selectProduct(id: string) {
    if (this.machine.getInventory().isAvailable(id)) {
      this.machine.setSelectedProduct(id);
      this.machine.setState(this.machine.getDispensingState());
    }
  }
 
  dispense() { console.log("Select a product first!"); }
}
 
// 3. Context Class
class VendingMachine {
  private idleState: State;
  private hasMoneyState: State;
  private dispensingState: State;
  
  private currentState: State;
  private balance: number = 0;
 
  constructor() {
    this.idleState = new IdleState(this);
    this.hasMoneyState = new HasMoneyState(this);
    this.dispensingState = new DispensingState(this); // Assume implementation
    this.currentState = this.idleState;
  }
 
  setState(state: State) { this.currentState = state; }
  addBalance(amount: number) { this.balance += amount; }
  
  // Delegate methods
  insertMoney(amount: number) { this.currentState.insertMoney(amount); }
  selectProduct(id: string) { this.currentState.selectProduct(id); }
 
  // Getters for states
  getHasMoneyState() { return this.hasMoneyState; }
  getDispensingState() { return this.dispensingState; }
}

Deep Dive: Why the State Pattern?

Without the State pattern, your VendingMachine would be riddled with complex if-else or switch statements:

// BAD: Conditionals everywhere
selectProduct(id: string) {
  if (this.state === "IDLE") { ... }
  else if (this.state === "HAS_MONEY") { ... }
  else if (this.state === "DISPENSING") { ... }
}

The State pattern encapsulates the logic for each state into its own class, making the system easy to maintain and extend (e.g., adding a "Maintenance" state).


Wrap Up

The Vending Machine is the gold standard for learning the State pattern. It demonstrates how to manage object transitions and keep your core logic clean even as the complexity of the machine grows.

[!TIP] In an interview, mention that each state class could be a Singleton if they don't hold any unique per-machine data, which saves memory!