Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
โฑ๏ธ 8 min read

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


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

  1. โ€œ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).

  2. โ€œ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.

  3. โ€œ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).

  4. โ€œ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

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access