State Machines in LLD
A state machine (finite state machine / FSM) is a model where an entity can be in exactly one state at any time, and transitions between states are triggered by specific events. The key insight: you define all valid transitions upfront, and any undeclared transition is automatically invalid.
Why this matters: Orders, elevators, vending machines, game entities, and payment transactions all have well-defined lifecycles. Modeling them as explicit state machines prevents invalid state transitions and makes the system behavior predictable and testable.
Prerequisites
- State Pattern โ OOP implementation of state machines
- Exception Handling โ handling invalid transitions
Two Implementation Approaches
flowchart LR
A["Enum + Transition Table<br/>Lightweight; data-driven"]:::client
B["State Pattern<br/>Heavyweight; behavior-driven"]:::service
A --> |"Behavior per state is simple"| C["Use Enum FSM"]:::data
B --> |"Behavior per state is complex"| D["Use State Pattern"]:::data
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
| Approach | Best When | Drawback |
|---|---|---|
| Enum + transition table | States are mostly about data flow, not complex behavior | Logic lives outside the states |
| State pattern | Each state has distinct, complex behavior | More classes to manage |
Approach 1: Enum-Based FSM with Transition Table
This is the lightest approach and works well for most interview problems.
// Define states
public enum OrderState {
PLACED, CONFIRMED, PREPARING, OUT_FOR_DELIVERY, DELIVERED, CANCELLED
}
// Define events that trigger transitions
public enum OrderEvent {
CONFIRM, START_PREPARING, DISPATCH, DELIVER, CANCEL
}
// State machine: encapsulates all valid transitions
public class OrderStateMachine {
// Transition table: (currentState, event) -> nextState
private static final Map<OrderState, Map<OrderEvent, OrderState>> TRANSITIONS;
static {
TRANSITIONS = new EnumMap<>(OrderState.class);
TRANSITIONS.put(OrderState.PLACED, Map.of(
OrderEvent.CONFIRM, OrderState.CONFIRMED,
OrderEvent.CANCEL, OrderState.CANCELLED
));
TRANSITIONS.put(OrderState.CONFIRMED, Map.of(
OrderEvent.START_PREPARING, OrderState.PREPARING,
OrderEvent.CANCEL, OrderState.CANCELLED
));
TRANSITIONS.put(OrderState.PREPARING, Map.of(
OrderEvent.DISPATCH, OrderState.OUT_FOR_DELIVERY,
OrderEvent.CANCEL, OrderState.CANCELLED
));
TRANSITIONS.put(OrderState.OUT_FOR_DELIVERY, Map.of(
OrderEvent.DELIVER, OrderState.DELIVERED
// Cannot cancel once dispatched
));
// DELIVERED and CANCELLED are terminal -- no transitions
TRANSITIONS.put(OrderState.DELIVERED, Map.of());
TRANSITIONS.put(OrderState.CANCELLED, Map.of());
}
public OrderState transition(OrderState current, OrderEvent event) {
Map<OrderEvent, OrderState> stateTransitions = TRANSITIONS.get(current);
if (stateTransitions == null || !stateTransitions.containsKey(event)) {
throw new InvalidTransitionException(current, event);
}
return stateTransitions.get(event);
}
public boolean canTransition(OrderState current, OrderEvent event) {
Map<OrderEvent, OrderState> stateTransitions = TRANSITIONS.get(current);
return stateTransitions != null && stateTransitions.containsKey(event);
}
public Set<OrderEvent> allowedEvents(OrderState current) {
return TRANSITIONS.getOrDefault(current, Map.of()).keySet();
}
}
// Usage in Order entity
public class Order {
private static final OrderStateMachine FSM = new OrderStateMachine();
private final String orderId;
private OrderState state;
private final List<StateTransition> history = new ArrayList<>();
public Order(String orderId) {
this.orderId = orderId;
this.state = OrderState.PLACED;
}
public void trigger(OrderEvent event) {
OrderState previousState = this.state;
this.state = FSM.transition(this.state, event);
history.add(new StateTransition(previousState, event, this.state, Instant.now()));
}
public OrderState getState() { return state; }
public boolean canDo(OrderEvent event) { return FSM.canTransition(state, event); }
}
from enum import Enum, auto
from dataclasses import dataclass, field
from datetime import datetime
class OrderState(Enum):
PLACED = auto()
CONFIRMED = auto()
PREPARING = auto()
OUT_FOR_DELIVERY = auto()
DELIVERED = auto()
CANCELLED = auto()
class OrderEvent(Enum):
CONFIRM = auto()
START_PREPARING = auto()
DISPATCH = auto()
DELIVER = auto()
CANCEL = auto()
class OrderStateMachine:
TRANSITIONS: dict[OrderState, dict[OrderEvent, OrderState]] = {
OrderState.PLACED: {
OrderEvent.CONFIRM: OrderState.CONFIRMED,
OrderEvent.CANCEL: OrderState.CANCELLED,
},
OrderState.CONFIRMED: {
OrderEvent.START_PREPARING: OrderState.PREPARING,
OrderEvent.CANCEL: OrderState.CANCELLED,
},
OrderState.PREPARING: {
OrderEvent.DISPATCH: OrderState.OUT_FOR_DELIVERY,
OrderEvent.CANCEL: OrderState.CANCELLED,
},
OrderState.OUT_FOR_DELIVERY: {
OrderEvent.DELIVER: OrderState.DELIVERED,
},
OrderState.DELIVERED: {},
OrderState.CANCELLED: {},
}
def transition(self, current: OrderState, event: OrderEvent) -> OrderState:
allowed = self.TRANSITIONS.get(current, {})
if event not in allowed:
raise InvalidTransitionError(
f"Cannot {event.name} from {current.name}"
)
return allowed[event]
def can_transition(self, current: OrderState, event: OrderEvent) -> bool:
return event in self.TRANSITIONS.get(current, {})
def allowed_events(self, current: OrderState) -> set[OrderEvent]:
return set(self.TRANSITIONS.get(current, {}).keys())
# Usage
class Order:
_fsm = OrderStateMachine()
def __init__(self, order_id: str):
self.order_id = order_id
self.state = OrderState.PLACED
self.history: list[tuple] = []
def trigger(self, event: OrderEvent):
previous = self.state
self.state = self._fsm.transition(self.state, event)
self.history.append((previous, event, self.state, datetime.now()))
def can_do(self, event: OrderEvent) -> bool:
return self._fsm.can_transition(self.state, event)
enum class OrderState {
PLACED, CONFIRMED, PREPARING, OUT_FOR_DELIVERY, DELIVERED, CANCELLED
};
enum class OrderEvent {
CONFIRM, START_PREPARING, DISPATCH, DELIVER, CANCEL
};
class OrderStateMachine {
using TransitionKey = pair<OrderState, OrderEvent>;
unordered_map<int, OrderState> transitions_;
static int key(OrderState s, OrderEvent e) {
return static_cast<int>(s) * 100 + static_cast<int>(e);
}
public:
OrderStateMachine() {
// PLACED transitions
transitions_[key(OrderState::PLACED, OrderEvent::CONFIRM)] = OrderState::CONFIRMED;
transitions_[key(OrderState::PLACED, OrderEvent::CANCEL)] = OrderState::CANCELLED;
// CONFIRMED transitions
transitions_[key(OrderState::CONFIRMED, OrderEvent::START_PREPARING)] = OrderState::PREPARING;
transitions_[key(OrderState::CONFIRMED, OrderEvent::CANCEL)] = OrderState::CANCELLED;
// PREPARING transitions
transitions_[key(OrderState::PREPARING, OrderEvent::DISPATCH)] = OrderState::OUT_FOR_DELIVERY;
transitions_[key(OrderState::PREPARING, OrderEvent::CANCEL)] = OrderState::CANCELLED;
// OUT_FOR_DELIVERY
transitions_[key(OrderState::OUT_FOR_DELIVERY, OrderEvent::DELIVER)] = OrderState::DELIVERED;
}
OrderState transition(OrderState current, OrderEvent event) const {
int k = key(current, event);
auto it = transitions_.find(k);
if (it == transitions_.end())
throw InvalidTransitionException(current, event);
return it->second;
}
bool canTransition(OrderState current, OrderEvent event) const {
return transitions_.count(key(current, event)) > 0;
}
};
class Order {
static OrderStateMachine fsm_;
string orderId_;
OrderState state_ = OrderState::PLACED;
public:
explicit Order(string id) : orderId_(std::move(id)) {}
void trigger(OrderEvent event) {
state_ = fsm_.transition(state_, event);
}
OrderState state() const { return state_; }
};
State Machine Diagram
stateDiagram-v2
[*] --> PLACED
PLACED --> CONFIRMED: confirm
PLACED --> CANCELLED: cancel
CONFIRMED --> PREPARING: startPreparing
CONFIRMED --> CANCELLED: cancel
PREPARING --> OUT_FOR_DELIVERY: dispatch
PREPARING --> CANCELLED: cancel
OUT_FOR_DELIVERY --> DELIVERED: deliver
DELIVERED --> [*]
CANCELLED --> [*]
Guard Conditions
Sometimes a transition is only valid if additional conditions are met (e.g., can only dispatch if a driver is assigned).
// Transition with guard condition
public class GuardedStateMachine {
@FunctionalInterface
public interface Guard {
boolean check(Order order);
}
private record Transition(OrderState target, Guard guard) {}
private final Map<OrderState, Map<OrderEvent, Transition>> transitions = new EnumMap<>(OrderState.class);
public GuardedStateMachine() {
addTransition(OrderState.PREPARING, OrderEvent.DISPATCH, OrderState.OUT_FOR_DELIVERY,
order -> order.getAssignedDriver() != null); // Guard: driver must be assigned
addTransition(OrderState.PLACED, OrderEvent.CANCEL, OrderState.CANCELLED,
order -> order.getPaymentStatus() != PaymentStatus.CAPTURED); // Guard: not yet captured
}
public OrderState transition(Order order, OrderEvent event) {
Transition t = transitions.getOrDefault(order.getState(), Map.of()).get(event);
if (t == null) throw new InvalidTransitionException(order.getState(), event);
if (!t.guard().check(order)) {
throw new GuardConditionFailedException(order.getState(), event);
}
return t.target();
}
}
from typing import Callable
class GuardedStateMachine:
def __init__(self):
# (state, event) -> (target_state, guard_fn)
self._transitions: dict[tuple, tuple[OrderState, Callable]] = {}
def add_transition(self, from_state, event, to_state, guard=None):
self._transitions[(from_state, event)] = (to_state, guard or (lambda o: True))
def transition(self, order: 'Order', event: OrderEvent) -> OrderState:
key = (order.state, event)
if key not in self._transitions:
raise InvalidTransitionError(f"No transition for {key}")
target, guard = self._transitions[key]
if not guard(order):
raise GuardConditionFailedError(f"Guard failed for {event.name}")
return target
# Setup
fsm = GuardedStateMachine()
fsm.add_transition(OrderState.PREPARING, OrderEvent.DISPATCH, OrderState.OUT_FOR_DELIVERY,
guard=lambda order: order.assigned_driver is not None)
using Guard = function<bool(const Order&)>;
struct Transition {
OrderState target;
Guard guard;
};
class GuardedStateMachine {
unordered_map<int, Transition> transitions_;
public:
void addTransition(OrderState from, OrderEvent event, OrderState to, Guard guard) {
transitions_[key(from, event)] = {to, std::move(guard)};
}
OrderState transition(const Order& order, OrderEvent event) {
auto it = transitions_.find(key(order.state(), event));
if (it == transitions_.end()) throw InvalidTransitionException();
if (!it->second.guard(order)) throw GuardConditionFailedException();
return it->second.target;
}
};
Enum FSM vs State Pattern: Decision Guide
| Factor | Use Enum FSM | Use State Pattern |
|---|---|---|
| Behavior per state | Minimal (just data transitions) | Significant (different methods per state) |
| Number of states | Any | 3-7 (more gets unwieldy) |
| Guard conditions | Easy to add | Embedded in state objects |
| Transition logic | Centralized in table | Distributed across state classes |
| Testing | Test the transition table | Test each state class |
| Interview time | Faster to implement | Slower but more OOP |
When to Use vs When to Avoid
| Use State Machines When | Avoid When |
|---|---|
| Entity has a clear lifecycle (created -> active -> done) | State is just a label with no behavior difference |
| Invalid transitions should be explicitly prevented | Any transition is valid at any time |
| State history or audit trail is needed | No one cares about state changes |
| Multiple events trigger different transitions from same state | Simple linear progression (just a counter) |
Interview Questions
-
โEnum FSM vs State Pattern?โ โ Enum FSM when transitions are the main concern and per-state behavior is minimal. State Pattern when each state has complex, distinct behavior (different methods do different things per state).
-
โHow do you handle async transitions?โ โ Add PENDING states (e.g., DISPATCH_PENDING). The trigger moves to pending, and a callback/event moves to the final state. This prevents accepting new events while an async operation is in-flight.
-
โHow do you test a state machine?โ โ Test every valid transition (happy path), test every invalid transition (should throw), test guard conditions (both pass and fail), test terminal states (no transitions allowed).
-
โWhat about hierarchical state machines?โ โ A state can contain sub-states (e.g., PREPARING has sub-states: COOKING, PACKING). Useful for complex systems but overkill for interviews.
See It in Action
| Vending Machine | Elevator | Order Management |