Engineering Atlas › Low-Level Design

Drawing UML state machine diagrams

14 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

States, events and transitions

A state is a period in an object's life during which a fixed set of rules applies. "Paid" is a state; "total is 42.10" is data. The test is whether the set of legal operations changes. An event happens at a point in time: a call (cancel()), a message (PaymentCaptured), a timer (after(30s)) or a condition becoming true (when(stock == 0)). A transition is an arrow between states triggered by an event. The black dot is the initial pseudostate and the circled dot a final state.

Guards and actions

The full label is event [guard] / action. The guard is a side-effect-free boolean evaluated when the event arrives; if it is false, the machine looks for another transition. The action runs while the transition fires. Two transitions on the same event from the same state need mutually exclusive guards; UML leaves the choice undefined, and real implementations quietly pick the first one declared. A choice pseudostate (a diamond) evaluates its guards after the first part of the transition has run, for branches that depend on a value the action just computed, such as "charge the card, then go to paid or payment_failed".

Entry, exit and do activities

Inside a state box you can write entry / startTimer(), exit / stopTimer() and do / pollSensor(). Entry runs every time the state is entered, whichever arrow brought you in, and exit every time it is left. With five arrows into connected, the "start heartbeat" code lives in one place, and nobody forgets it on the sixth arrow. A do activity is long-running work interrupted when the state is left: in code, a goroutine or timer that the exit action cancels.

An internal transition (event / action written inside the box) runs an action without leaving the state, so neither entry nor exit runs. A self-transition leaves and re-enters, so both run. That difference matters when entry resets a timer.

Composite states

A composite (or superstate) contains its own nested state machine. When the object is in playing, inside active, inside on, it is in all three at once: the active configuration. Two rules give composites their power:

  1. Inherited transitions. An event is offered to the innermost active state first, then to its parent, then the grandparent. The power arrow drawn once on on works from every state inside it. This is Harel's 1987 answer to state explosion: N states that all need an emergency exit need N flat arrows but one hierarchical arrow.
  2. Entering a composite enters a child. An arrow that ends on the border of a composite continues to its initial substate, recursively, until a simple (leaf) state is active. An arrow can also cross the border and end on a specific inner state.

When a transition fires, the machine finds the innermost state that strictly contains both source and target (the least common ancestor, or LCA). It exits every active state below the LCA, innermost first, enters each state from the LCA down to the target, outermost first, then drills down through initial substates. Inside-out exits mean a child never tears down after its parent has released shared resources; outside-in entries mean a parent's setup is in place before a child uses it.

History

A shallow history pseudostate (H in a circle) says: on re-entry, resume the direct child that was active when you last left. If on has history and was left from active > paused, re-entry resumes active, which restarts at its own initial child. Deep history (H*) restores the whole nested configuration. A composite never visited falls back to its initial state.

History is how wizards resume at the right step. The catch is that it is hidden state: two objects in the "same" state can behave differently depending on what they remember, so test it explicitly.

Orthogonal regions

A composite can be split by dashed lines into regions that run concurrently, such as a fan region (off, low, high) and an independent ac region (off, on) in climate control. Without regions you need the cross product, six states. Every event is offered to all regions, and "what state is it in?" now returns a set, which persistence and logging must handle.

From diagram to code

Flat diagrams map to a switch on an enum, a (state, event) transition table (Enums) or one class per state (State). Hierarchy is where hand-written code breaks: inherited transitions become copy-pasted if branches in every child, and entry and exit order becomes whatever the developer remembered. If you need composites, use a statechart runtime (XState, Qt's state machine framework, SCXML engines, Miro Samek's QP) or a small interpreter like the one in Practice, and generate both the diagram and the tests from one definition.

Reviewing agent-written code

Code assistants produce a plausible switch and miss the semantics around it. Check that unhandled events are explicitly ignored or rejected rather than falling into a default branch that mutates state, that entry actions are not duplicated per incoming arrow, that guards have no side effects, and that self and internal transitions are not confused. Ask for the diagram and a test per arrow.

The same shape elsewhere

Statecharts are a multi-step consistency tool: they make the legal orders of operations explicit. The same shape appears in TCP's connection states, a payment saga's compensation steps and a checkout form's UI, where hierarchy lets "connection reset" or "user logged out" be handled once at the top.

Down the stack

Layer 1The contract

UML specifies run-to-completion: events are queued and each is fully handled (guards, exits, actions, entries, drill-down) before the next is dequeued, so no one observes a half-finished step. The transition chosen is the one on the innermost active state with an enabled guard; exits run innermost first, entries outermost first, and an event with no enabled transition is discarded. Given a definition and an event sequence, the log of exits and entries is fully determined, which makes it testable.

Layer 2The mechanism

A hierarchical machine is a tree of states plus a transition list. Dispatch walks from the active leaf up parent pointers, testing transitions at each level. Executing one computes the LCA from the two ancestor chains, exits up to it, enters down from it, then follows history records or initial children to a leaf. History is a map from composite to last active child, written during exit.

SCXML, the W3C statechart XML format, specifies this algorithm in pseudocode (microstep, exitStates, enterStates, computeExitSet), including document order as the tiebreaker between conflicting transitions. Samek's QP implements hierarchy with each state as a C function that returns either "handled", "transition to X" or "super is Y"; the dispatcher discovers the hierarchy at run time by calling those functions. XState stores the machine as a JSON-like config, interprets it, and exposes the active configuration as a value you can serialise.

Layer 3The machine

A flat table of 50 states by 20 events with one-byte targets is 1 KB and sits in L1 cache, so a lookup takes nanoseconds. Hierarchical dispatch adds a parent-pointer hop per level, and state functions or virtual methods (Go itabs, C++ vtables) add an indirect call each: 1 to 2 ns predicted, 15 to 20 cycles mispredicted. An interpreted JavaScript config walks object properties and prototype chains, orders of magnitude slower than a table but irrelevant for 60 UI events a second. Lexers that run per byte compile their machines to jump tables or goto instead (Ragel, re2c).

Run-to-completion is also the concurrency model: one goroutine or event loop owns the machine and others send events through a channel, so no lock is held while actions run. A slow action delays every event queued behind it.

Why it's built this way

  • Behaviour that depends on history is hard to reason about in scattered flags, so you name each period of life as a state.
  • Flat machines explode when many states share a reaction, so you nest them and let parents handle shared events.
  • Nesting makes the order of setup and teardown ambiguous, so the semantics fix it: exits inside-out, entries outside-in, around the LCA.
  • Interleaved events would expose half-finished transitions, so each event runs to completion before the next.
  • Users and flows return to where they were, so composites can remember their last child through history.

Diagram

stateDiagram-v2
  [*] --> Off
  Off --> On: power
  On --> Off: power
  On --> Error: fault
  Error --> Off: reset
  state On {
    [*] --> Idle
    Idle --> Active: play [hasMedia]
    Active --> Idle: stop
    state Active {
      [*] --> Playing
      Playing --> Paused: pause
      Paused --> Playing: play
      Playing --> Playing: tick
    }
  }

Mermaid has no history pseudostate, so the shallow history on On used in the worked example is stated in prose here; in a full UML tool it would be an H inside the On box.

Worked example

Take the player above with shallow history on On. Power from Off logs exit:Off, enter:On, enter:Idle. Play with no media fails its guard, nothing up the chain handles it, and it is dropped. With media, play logs exit:Idle, enter:Active, enter:Playing (the LCA is On), and pause logs exit:Playing, enter:Paused.

Now press power. Only On handles it, so the machine exits Paused, Active (recording it as On's last child) and On, then enters Off. Press power again: enter:On, history picks Active over Idle, and because history is shallow Active starts at Playing. The user returns playing, not paused; deep history would have restored Paused. Whichever you choose, it is a product decision the diagram makes visible.

A tick in Playing is a self-transition, so it logs exit:Playing, enter:Playing and re-runs entry: a bug if entry restarts a buffering timer and tick was meant to be internal.

Trade-offs

OptionStrengthsWeaknessesChoose when
Flat diagram, switch or tableEasy to read and test; trivially serialisedShared reactions repeated in every stateUnder about 10 states with few cross-cutting events
Composite statesOne arrow for shared reactions; entry and exit scoped by levelExit and entry order must be understood; dispatch walks parentsMany states share cancel, timeout or reset handling
History statesResumes flows without extra flagsHidden memory; two equal-looking states behave differentlyUsers leave and return to a sub-flow
Orthogonal regionsAvoids cross-product statesConfiguration is a set; harder to persist and logTruly independent aspects of one object
Statechart runtime (XState, SCXML, QP)Correct semantics, visualisers, simulatorsDependency, learning curve, interpretation overheadHierarchy, history or regions are needed in production

Pitfalls in production

Leadership lens

Check yourself

  1. The active configuration is On > Active > Paused, and only On handles power. In what order do exit actions run, and why that order?

    Show answer

    Paused, then Active, then On. Exiting inside-out guarantees a child tears down before the parent whose resources it may depend on.

  2. What is the difference between a self-transition and an internal transition on the same state?

    Show answer

    A self-transition leaves and re-enters the state, so exit and entry actions run. An internal transition only runs its action and leaves the state, and its entry and exit, untouched.

  3. On has shallow history and was last left from Active > Paused. Re-entering On through history ends in which leaf?

    Show answer

    Playing. Shallow history restores the direct child Active, and Active then starts at its own initial child. Deep history would restore Paused.

  4. Why does run-to-completion make concurrency easier rather than harder?

    Show answer

    Only one event is processed at a time by one owner, so no transition ever observes another half-finished transition. Other threads communicate by enqueueing events instead of sharing state behind locks.

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →