State Pattern
The State pattern lets an object change its behavior when its internal state changes. Instead of giant if (state == X) conditionals scattered throughout the class, each state becomes its own object with its own behavior. The context delegates to whichever state is currently active.
Why this matters: Vending machines, elevators, order systems, and game entities all have lifecycles with distinct states. Interviewers watch for whether you model state transitions cleanly or bury them in nested conditionals.
Prerequisites
- Strategy Pattern โ same structure, different intent
- State Machines โ the concept behind this pattern
Class Diagram
classDiagram
class VendingMachine {
-currentState: State
+insertCoin(amount)
+selectProduct(code)
+dispense()
+setState(State)
}
class State {
<<interface>>
+insertCoin(machine, amount)
+selectProduct(machine, code)
+dispense(machine)
}
class IdleState {
+insertCoin(machine, amount)
+selectProduct(machine, code)
+dispense(machine)
}
class HasMoneyState {
+insertCoin(machine, amount)
+selectProduct(machine, code)
+dispense(machine)
}
class DispensingState {
+insertCoin(machine, amount)
+selectProduct(machine, code)
+dispense(machine)
}
VendingMachine --> State
State <|.. IdleState
State <|.. HasMoneyState
State <|.. DispensingState
The Problem: Conditional Explosion
stateDiagram-v2
[*] --> Idle
Idle --> HasMoney: insertCoin
HasMoney --> HasMoney: insertCoin - add more
HasMoney --> Dispensing: selectProduct
HasMoney --> Idle: cancelAndRefund
Dispensing --> Idle: dispenseComplete
Without the State pattern, every method checks the current state:
// BEFORE: every method has state checks -- this gets unmanageable fast
public class VendingMachine {
private enum MachineState { IDLE, HAS_MONEY, DISPENSING }
private MachineState state = MachineState.IDLE;
private int balance = 0;
public void insertCoin(int amount) {
if (state == MachineState.IDLE) {
balance = amount;
state = MachineState.HAS_MONEY;
} else if (state == MachineState.HAS_MONEY) {
balance += amount;
} else if (state == MachineState.DISPENSING) {
System.out.println("Wait, dispensing in progress");
}
}
public void selectProduct(String code) {
if (state == MachineState.IDLE) {
System.out.println("Insert coin first");
} else if (state == MachineState.HAS_MONEY) {
Product p = catalog.get(code);
if (p != null && balance >= p.getPrice()) {
state = MachineState.DISPENSING;
// dispense logic...
} else {
System.out.println("Insufficient balance");
}
} else if (state == MachineState.DISPENSING) {
System.out.println("Wait, dispensing in progress");
}
}
// Every new state multiplies every method by another branch
}
# BEFORE: nested conditionals everywhere
class VendingMachine:
def __init__(self):
self.state = "IDLE"
self.balance = 0
def insert_coin(self, amount):
if self.state == "IDLE":
self.balance = amount
self.state = "HAS_MONEY"
elif self.state == "HAS_MONEY":
self.balance += amount
elif self.state == "DISPENSING":
print("Wait, dispensing in progress")
def select_product(self, code):
if self.state == "IDLE":
print("Insert coin first")
elif self.state == "HAS_MONEY":
product = self.catalog.get(code)
if product and self.balance >= product.price:
self.state = "DISPENSING"
else:
print("Insufficient balance")
elif self.state == "DISPENSING":
print("Wait, dispensing in progress")
// BEFORE: switch in every method
void VendingMachine::insertCoin(int amount) {
switch (state_) {
case IDLE:
balance_ = amount;
state_ = HAS_MONEY;
break;
case HAS_MONEY:
balance_ += amount;
break;
case DISPENSING:
cout << "Wait, dispensing in progress" << endl;
break;
}
}
// Repeat for selectProduct(), dispense(), cancel()...
The Solution: State Pattern
// State interface -- one method per possible action
public interface VendingState {
void insertCoin(VendingMachine machine, int amount);
void selectProduct(VendingMachine machine, String code);
void dispense(VendingMachine machine);
void cancel(VendingMachine machine);
}
// Idle: waiting for coins
public class IdleState implements VendingState {
@Override
public void insertCoin(VendingMachine machine, int amount) {
machine.addBalance(amount);
machine.setState(new HasMoneyState());
}
@Override
public void selectProduct(VendingMachine machine, String code) {
throw new IllegalStateException("Insert coin first");
}
@Override
public void dispense(VendingMachine machine) {
throw new IllegalStateException("Nothing to dispense");
}
@Override
public void cancel(VendingMachine machine) {
// Nothing to cancel in idle
}
}
// HasMoney: coins inserted, waiting for product selection
public class HasMoneyState implements VendingState {
@Override
public void insertCoin(VendingMachine machine, int amount) {
machine.addBalance(amount); // accumulate
}
@Override
public void selectProduct(VendingMachine machine, String code) {
Product product = machine.getProduct(code);
if (product == null) {
throw new ProductNotFoundException(code);
}
if (machine.getBalance() < product.getPrice()) {
throw new InsufficientBalanceException(machine.getBalance(), product.getPrice());
}
machine.setSelectedProduct(product);
machine.setState(new DispensingState());
}
@Override
public void dispense(VendingMachine machine) {
throw new IllegalStateException("Select a product first");
}
@Override
public void cancel(VendingMachine machine) {
int refund = machine.getBalance();
machine.resetBalance();
machine.setState(new IdleState());
machine.returnChange(refund);
}
}
// Dispensing: product being delivered
public class DispensingState implements VendingState {
@Override
public void insertCoin(VendingMachine machine, int amount) {
throw new IllegalStateException("Dispensing in progress, please wait");
}
@Override
public void selectProduct(VendingMachine machine, String code) {
throw new IllegalStateException("Dispensing in progress");
}
@Override
public void dispense(VendingMachine machine) {
Product product = machine.getSelectedProduct();
machine.deductBalance(product.getPrice());
machine.deliverProduct(product);
int change = machine.getBalance();
if (change > 0) {
machine.returnChange(change);
machine.resetBalance();
}
machine.setState(new IdleState());
}
@Override
public void cancel(VendingMachine machine) {
throw new IllegalStateException("Cannot cancel during dispensing");
}
}
// Context: clean delegation, no conditionals
public class VendingMachine {
private VendingState currentState;
private int balance;
private Product selectedProduct;
private final Map<String, Product> inventory;
public VendingMachine(Map<String, Product> inventory) {
this.inventory = inventory;
this.currentState = new IdleState();
}
public void insertCoin(int amount) { currentState.insertCoin(this, amount); }
public void selectProduct(String code) { currentState.selectProduct(this, code); }
public void dispense() { currentState.dispense(this); }
public void cancel() { currentState.cancel(this); }
// Package-private state management methods
void setState(VendingState state) { this.currentState = state; }
void addBalance(int amount) { this.balance += amount; }
void deductBalance(int amount) { this.balance -= amount; }
void resetBalance() { this.balance = 0; }
int getBalance() { return balance; }
Product getProduct(String code) { return inventory.get(code); }
void setSelectedProduct(Product p) { this.selectedProduct = p; }
Product getSelectedProduct() { return selectedProduct; }
void deliverProduct(Product p) { /* actuator logic */ }
void returnChange(int amount) { /* coin return mechanism */ }
}
from abc import ABC, abstractmethod
class VendingState(ABC):
@abstractmethod
def insert_coin(self, machine: 'VendingMachine', amount: int): ...
@abstractmethod
def select_product(self, machine: 'VendingMachine', code: str): ...
@abstractmethod
def dispense(self, machine: 'VendingMachine'): ...
@abstractmethod
def cancel(self, machine: 'VendingMachine'): ...
class IdleState(VendingState):
def insert_coin(self, machine, amount):
machine.add_balance(amount)
machine.set_state(HasMoneyState())
def select_product(self, machine, code):
raise InvalidOperationError("Insert coin first")
def dispense(self, machine):
raise InvalidOperationError("Nothing to dispense")
def cancel(self, machine):
pass # Nothing to cancel
class HasMoneyState(VendingState):
def insert_coin(self, machine, amount):
machine.add_balance(amount)
def select_product(self, machine, code):
product = machine.get_product(code)
if not product:
raise ProductNotFoundError(code)
if machine.balance < product.price:
raise InsufficientBalanceError(machine.balance, product.price)
machine.selected_product = product
machine.set_state(DispensingState())
def dispense(self, machine):
raise InvalidOperationError("Select a product first")
def cancel(self, machine):
refund = machine.balance
machine.reset_balance()
machine.set_state(IdleState())
machine.return_change(refund)
class DispensingState(VendingState):
def insert_coin(self, machine, amount):
raise InvalidOperationError("Dispensing in progress")
def select_product(self, machine, code):
raise InvalidOperationError("Dispensing in progress")
def dispense(self, machine):
product = machine.selected_product
machine.deduct_balance(product.price)
machine.deliver_product(product)
if machine.balance > 0:
machine.return_change(machine.balance)
machine.reset_balance()
machine.set_state(IdleState())
def cancel(self, machine):
raise InvalidOperationError("Cannot cancel during dispensing")
class VendingMachine:
def __init__(self, inventory: dict):
self._state = IdleState()
self.balance = 0
self.selected_product = None
self._inventory = inventory
def insert_coin(self, amount): self._state.insert_coin(self, amount)
def select_product(self, code): self._state.select_product(self, code)
def dispense(self): self._state.dispense(self)
def cancel(self): self._state.cancel(self)
def set_state(self, state: VendingState): self._state = state
def add_balance(self, amount): self.balance += amount
def deduct_balance(self, amount): self.balance -= amount
def reset_balance(self): self.balance = 0
def get_product(self, code): return self._inventory.get(code)
def deliver_product(self, p): pass
def return_change(self, amount): pass
class VendingMachine; // forward declaration
class VendingState {
public:
virtual ~VendingState() = default;
virtual void insertCoin(VendingMachine& machine, int amount) = 0;
virtual void selectProduct(VendingMachine& machine, const string& code) = 0;
virtual void dispense(VendingMachine& machine) = 0;
virtual void cancel(VendingMachine& machine) = 0;
};
class IdleState : public VendingState {
public:
void insertCoin(VendingMachine& machine, int amount) override;
void selectProduct(VendingMachine& machine, const string& code) override {
throw runtime_error("Insert coin first");
}
void dispense(VendingMachine& machine) override {
throw runtime_error("Nothing to dispense");
}
void cancel(VendingMachine& machine) override { /* no-op */ }
};
class HasMoneyState : public VendingState {
public:
void insertCoin(VendingMachine& machine, int amount) override;
void selectProduct(VendingMachine& machine, const string& code) override;
void dispense(VendingMachine& machine) override {
throw runtime_error("Select a product first");
}
void cancel(VendingMachine& machine) override;
};
class VendingMachine {
unique_ptr<VendingState> state_;
int balance_ = 0;
Product* selectedProduct_ = nullptr;
unordered_map<string, Product> inventory_;
public:
explicit VendingMachine(unordered_map<string, Product> inv)
: inventory_(std::move(inv)), state_(make_unique<IdleState>()) {}
void insertCoin(int amount) { state_->insertCoin(*this, amount); }
void selectProduct(const string& code) { state_->selectProduct(*this, code); }
void dispense() { state_->dispense(*this); }
void cancel() { state_->cancel(*this); }
void setState(unique_ptr<VendingState> s) { state_ = std::move(s); }
void addBalance(int amount) { balance_ += amount; }
int getBalance() const { return balance_; }
void resetBalance() { balance_ = 0; }
Product* getProduct(const string& code) { return &inventory_[code]; }
};
When to Use vs When to Avoid
| Use State Pattern When | Avoid When |
|---|---|
| Object has 3+ distinct behavioral states | States are just a status field with no behavior differences |
| Behavior differs significantly per state | All states behave the same way |
| State transitions follow specific rules | Transitions are trivial (one direction only) |
| You want to eliminate state-checking conditionals | A simple enum + switch is readable enough |
| New states might be added later | The state set is tiny and fixed forever |
Interview Questions
-
โHow is State different from Strategy?โ โ In Strategy, the client chooses the algorithm. In State, the object transitions itself internally. States know about valid transitions; strategies are independent of each other.
-
โWhere does the transition logic live?โ โ In the state objects themselves. Each state knows what transitions are valid and creates the next state. The context just calls
setState(). -
โWhat about invalid transitions?โ โ Throw an exception or return an error. The pattern makes invalid actions explicit โ calling
dispense()inIdleStateclearly throws, rather than silently returning nothing. -
โShould states be singletons or new instances?โ โ If states are stateless, share them as singletons. If they carry data (like timestamps or counters), create fresh instances per transition.
See It in Action
| Vending Machine | Elevator | Tic-Tac-Toe |