State Machine Implementation

Type: Software Reference Confidence: 0.92 Sources: 7 Verified: 2026-02-24 Freshness: 2026-02-24

TL;DR

Constraints

Quick Reference

ApproachComplexityType SafetyPersistenceBest For
Switch/case on enumLowMediumManualSimple flows, 3-5 states, no hierarchy
State transition table (map)Low-MediumHighEasy to serializeFlat FSMs, config-driven flows
State pattern (OOP)MediumHighManualOpen/closed principle, per-state behavior
XState / StatechartsMedium-HighVery HighBuilt-in (inspect, persist)Complex UI flows, hierarchical/parallel states
DB-backed workflowHighMediumNative (row = state)Order lifecycles, multi-step approval flows
Coroutine/generator-basedMediumLow-MediumDifficultStreaming parsers, protocol handlers
ComponentRoleWhen to Add
States (enum)All valid configurationsAlways — define first
Events (enum/type)Triggers that cause transitionsAlways — finite set of inputs
Transition tableMaps (state, event) -> nextStateAlways — the core of the FSM
GuardsBoolean predicates that allow/block transitionsWhen transitions are conditional
Entry/exit actionsSide effects on state entry or exitWhen states trigger work (API calls, notifications)
Context (extended state)Mutable data carried alongside discrete stateWhen you need counters, retries, accumulated data
Hierarchical statesNested state machines (substates)When states share common transitions
Parallel statesOrthogonal regions executing simultaneouslyWhen independent concerns evolve independently

Decision Tree

START
|-- How many states?
|   |-- 2-3 simple states, no guards
|   |   --> switch/case on enum (simplest, inline)
|   |-- 4-10 states, flat transitions
|   |   |-- Need runtime config or serialization?
|   |   |   |-- YES --> State transition table (map/dict)
|   |   |   |-- NO --> State pattern (OOP) or switch/case
|   |-- 10+ states OR hierarchical/parallel
|   |   --> XState / statechart library
|-- Does state survive restarts?
|   |-- YES, short-lived (minutes)
|   |   --> Serialize state to Redis/cache
|   |-- YES, long-lived (hours/days)
|   |   --> DB-backed workflow with row-level state
|   |-- NO
|   |   --> In-memory FSM
|-- Is this a parser or protocol handler?
|   |-- YES --> Coroutine/generator-based FSM
|   |-- NO --> One of the above based on complexity

Step-by-Step Guide

1. Define your states as an exhaustive enum

Enumerate every valid state your system can be in. Do not leave room for implicit states. [src3]

// TypeScript: use a string literal union for exhaustive checking
type OrderState = 'idle' | 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';

Verify: Confirm every state is reachable and every state has at least one exit transition (except terminal states).

2. Define your events

List every trigger that can cause a state change. Events are the inputs to your FSM. [src1]

type OrderEvent =
  | { type: 'PLACE_ORDER'; items: string[] }
  | { type: 'CONFIRM_PAYMENT' }
  | { type: 'SHIP'; trackingId: string }
  | { type: 'DELIVER' }
  | { type: 'CANCEL'; reason: string };

Verify: Every event has at least one state that handles it. Unhandled events should be explicitly rejected.

3. Build the transition table

Map every valid (currentState, event) pair to its nextState. This is the core of your FSM. [src7]

const transitions: Record<OrderState, Partial<Record<OrderEvent['type'], OrderState>>> = {
  idle:      { PLACE_ORDER: 'pending' },
  pending:   { CONFIRM_PAYMENT: 'confirmed', CANCEL: 'cancelled' },
  confirmed: { SHIP: 'shipped', CANCEL: 'cancelled' },
  shipped:   { DELIVER: 'delivered' },
  delivered: {},
  cancelled: {},
};

Verify: Count transitions. For N states and M events, you should have far fewer than N*M entries.

4. Implement the transition function

Write a single function that looks up the transition, applies guards, and returns the new state. [src4]

function transition(current: OrderState, event: OrderEvent): OrderState {
  const nextState = transitions[current]?.[event.type];
  if (!nextState) {
    throw new Error(`Invalid transition: ${current} + ${event.type}`);
  }
  return nextState;
}

Verify: transition('idle', { type: 'PLACE_ORDER', items: ['A'] }) returns 'pending'. Invalid transitions throw.

5. Add guards and actions

Guards gate transitions; actions execute side effects on entry, exit, or transition. Keep guards pure and actions idempotent. [src1]

// Guard: only allow cancellation if not yet shipped
function canCancel(state: OrderState): boolean {
  return state === 'pending' || state === 'confirmed';
}

// Action: send notification on state entry (idempotent)
function onEnterShipped(context: { trackingId: string }): void {
  sendTrackingEmail(context.trackingId);
}

Verify: Test guards return correct boolean for each state. Test actions are idempotent.

Code Examples

TypeScript/XState: Order Lifecycle

// Input:  XState v5 installed (npm install xstate)
// Output: Type-safe order state machine with guards and actions

import { createMachine, createActor } from 'xstate';

const orderMachine = createMachine({
  id: 'order',
  initial: 'idle',
  context: { items: [] as string[], trackingId: '' },
  states: {
    idle: {
      on: { PLACE_ORDER: { target: 'pending', actions: 'setItems' } }
    },
    pending: {
      on: {
        CONFIRM_PAYMENT: 'confirmed',
        CANCEL: 'cancelled'
      }
    },
    confirmed: {
      on: {
        SHIP: { target: 'shipped', actions: 'setTracking' },
        CANCEL: 'cancelled'
      }
    },
    shipped: {
      on: { DELIVER: 'delivered' },
      entry: 'notifyShipped'
    },
    delivered: { type: 'final' },
    cancelled: { type: 'final' }
  }
});

const actor = createActor(orderMachine).start();
actor.send({ type: 'PLACE_ORDER', items: ['Widget'] });
console.log(actor.getSnapshot().value); // 'pending'

Python: Transition Table FSM

# Input:  Python 3.10+ (match/case syntax)
# Output: Strict FSM that rejects invalid transitions

from enum import Enum, auto
from dataclasses import dataclass, field

class State(Enum):
    IDLE = auto()
    PENDING = auto()
    CONFIRMED = auto()
    SHIPPED = auto()
    DELIVERED = auto()
    CANCELLED = auto()

class Event(Enum):
    PLACE_ORDER = auto()
    CONFIRM_PAYMENT = auto()
    SHIP = auto()
    DELIVER = auto()
    CANCEL = auto()

@dataclass
class StateMachine:
    state: State = State.IDLE
    _transitions: dict[tuple[State, Event], State] = field(
        default_factory=lambda: {
            (State.IDLE, Event.PLACE_ORDER): State.PENDING,
            (State.PENDING, Event.CONFIRM_PAYMENT): State.CONFIRMED,
            (State.PENDING, Event.CANCEL): State.CANCELLED,
            (State.CONFIRMED, Event.SHIP): State.SHIPPED,
            (State.CONFIRMED, Event.CANCEL): State.CANCELLED,
            (State.SHIPPED, Event.DELIVER): State.DELIVERED,
        }
    )

    def send(self, event: Event) -> State:
        key = (self.state, event)
        if key not in self._transitions:
            raise ValueError(
                f"Invalid transition: {self.state.name} + {event.name}"
            )
        self.state = self._transitions[key]
        return self.state

# Usage
fsm = StateMachine()
fsm.send(Event.PLACE_ORDER)       # -> State.PENDING
fsm.send(Event.CONFIRM_PAYMENT)   # -> State.CONFIRMED

Go: Interface-Based State Pattern

// Input:  Go 1.21+
// Output: State machine using interfaces for open/closed extensibility

package statemachine

import "fmt"

type Event string

const (
    PlaceOrder     Event = "PLACE_ORDER"
    ConfirmPayment Event = "CONFIRM_PAYMENT"
    Ship           Event = "SHIP"
    Deliver        Event = "DELIVER"
    Cancel         Event = "CANCEL"
)

type State interface {
    Name() string
    Handle(event Event) (State, error)
}

type IdleState struct{}
func (s *IdleState) Name() string { return "idle" }
func (s *IdleState) Handle(e Event) (State, error) {
    if e == PlaceOrder {
        return &PendingState{}, nil
    }
    return nil, fmt.Errorf("invalid event %s in state %s", e, s.Name())
}

type PendingState struct{}
func (s *PendingState) Name() string { return "pending" }
func (s *PendingState) Handle(e Event) (State, error) {
    switch e {
    case ConfirmPayment:
        return &ConfirmedState{}, nil
    case Cancel:
        return &CancelledState{}, nil
    default:
        return nil, fmt.Errorf("invalid event %s in state %s", e, s.Name())
    }
}

type OrderFSM struct {
    Current State
}

func NewOrderFSM() *OrderFSM {
    return &OrderFSM{Current: &IdleState{}}
}

func (fsm *OrderFSM) Send(event Event) error {
    next, err := fsm.Current.Handle(event)
    if err != nil {
        return err
    }
    fsm.Current = next
    return nil
}

Java: DB-Backed Order Lifecycle

// Input:  Java 17+, JPA/JDBC
// Output: Database-persisted state machine with transactional transitions

public enum OrderState {
    IDLE, PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED;

    private static final Map<OrderState, Map<String, OrderState>> TRANSITIONS = Map.of(
        IDLE,      Map.of("PLACE_ORDER", PENDING),
        PENDING,   Map.of("CONFIRM_PAYMENT", CONFIRMED, "CANCEL", CANCELLED),
        CONFIRMED, Map.of("SHIP", SHIPPED, "CANCEL", CANCELLED),
        SHIPPED,   Map.of("DELIVER", DELIVERED),
        DELIVERED, Map.of(),
        CANCELLED, Map.of()
    );

    public OrderState transition(String event) {
        Map<String, OrderState> allowed = TRANSITIONS.getOrDefault(this, Map.of());
        OrderState next = allowed.get(event);
        if (next == null) {
            throw new IllegalStateException(
                "Invalid transition: " + this + " + " + event
            );
        }
        return next;
    }
}

// Service layer — wrap in a transaction
@Transactional
public Order processEvent(Long orderId, String event) {
    Order order = orderRepo.findById(orderId)
        .orElseThrow(() -> new NotFoundException("Order not found"));
    OrderState newState = order.getState().transition(event);
    order.setState(newState);
    order.setUpdatedAt(Instant.now());
    return orderRepo.save(order); // @Version for optimistic locking
}

Anti-Patterns

Wrong: Boolean flags instead of explicit states

// BAD -- boolean flags create 2^N possible combinations, most of which are invalid
interface Order {
  isPending: boolean;
  isConfirmed: boolean;
  isShipped: boolean;
  isCancelled: boolean;
}
// What does { isPending: true, isShipped: true, isCancelled: true } mean?

Correct: Single state enum

// GOOD -- exactly N valid states, no impossible combinations
interface Order {
  state: 'idle' | 'pending' | 'confirmed' | 'shipped' | 'delivered' | 'cancelled';
}

Wrong: Implicit transitions via direct state mutation

# BAD -- anyone can set any state at any time; no validation
class Order:
    def __init__(self):
        self.state = "idle"

    def ship(self):
        self.state = "shipped"  # No check: can "ship" a cancelled order

order = Order()
order.state = "delivered"  # Direct mutation bypasses all rules

Correct: Transitions enforced through a single function

# GOOD -- all state changes go through validated transition function
class Order:
    def __init__(self):
        self._state = State.IDLE

    @property
    def state(self):
        return self._state

    def send(self, event):
        key = (self._state, event)
        if key not in TRANSITIONS:
            raise ValueError(f"Cannot {event.name} from {self._state.name}")
        self._state = TRANSITIONS[key]
        return self._state

Wrong: Missing guard conditions on transitions

// BAD -- transition always succeeds regardless of business rules
public OrderState ship(Order order) {
    order.setState(OrderState.SHIPPED); // No guard: 0-item order can be "shipped"
    return order.getState();
}

Correct: Guards validate preconditions before transitioning

// GOOD -- guard ensures business invariants before state change
public OrderState ship(Order order) {
    if (order.getItems().isEmpty()) {
        throw new IllegalStateException("Cannot ship an order with no items");
    }
    if (order.getState() != OrderState.CONFIRMED) {
        throw new IllegalStateException("Can only ship confirmed orders");
    }
    order.setState(OrderState.SHIPPED);
    return order.getState();
}

Common Pitfalls

When to Use / When Not to Use

Use WhenDon't Use WhenUse Instead
System has 3+ distinct states with defined transitionsOnly 2 states (on/off, enabled/disabled)Simple boolean or enum toggle
Multiple boolean flags create impossible combinationsState changes are trivial and linear (A -> B -> C, no branching)Simple sequential pipeline
Correctness is critical (payments, auth, workflows)UI component with simple show/hide logicConditional rendering / CSS classes
You need to persist and resume long-running processesReal-time streaming data transformationStream processors (RxJS, Kafka Streams)
Business rules dictate which transitions are allowedFull workflow with parallel branches, compensation, timersWorkflow engine (Temporal, Camunda)
You want to visualize and communicate system behaviorNeed probabilistic or fuzzy state transitionsMarkov chains, ML models

Important Caveats

Related Units