Drawing UML state machine diagrams
Open in the interactive app: practice, quiz, mock interview →In one minute
- A state machine diagram shows how one object (an order, a connection, a media player) reacts to events over its lifetime: its states, the events that move it, the guards that must hold and the actions that run.
- A transition reads
event [guard] / action. An event with no enabled transition is ignored by default, a rule behind many bugs in hand-written machines. - Composite states make diagrams scale. An arrow drawn on a parent applies to every state nested inside it, so "power off from anywhere" is one arrow, not twelve. Exits run innermost first, entries outermost first.
- History lets a composite remember where it was: shallow history restores the last direct child, deep history the whole nested configuration.
- Treat the diagram as a testable contract: each arrow is a test, each missing arrow a decision to write down. State and Enums cover implementing flat machines; this lesson covers notation and hierarchical semantics.
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:
- Inherited transitions. An event is offered to the innermost active state first, then to its parent, then the grandparent. The
powerarrow drawn once ononworks 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. - 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
| Option | Strengths | Weaknesses | Choose when |
|---|---|---|---|
| Flat diagram, switch or table | Easy to read and test; trivially serialised | Shared reactions repeated in every state | Under about 10 states with few cross-cutting events |
| Composite states | One arrow for shared reactions; entry and exit scoped by level | Exit and entry order must be understood; dispatch walks parents | Many states share cancel, timeout or reset handling |
| History states | Resumes flows without extra flags | Hidden memory; two equal-looking states behave differently | Users leave and return to a sub-flow |
| Orthogonal regions | Avoids cross-product states | Configuration is a set; harder to persist and log | Truly independent aspects of one object |
| Statechart runtime (XState, SCXML, QP) | Correct semantics, visualisers, simulators | Dependency, learning curve, interpretation overhead | Hierarchy, history or regions are needed in production |
Pitfalls in production
- Silent ignores. "I clicked cancel and nothing happened." Decide per event whether to ignore, reject or defer, and log ignored events.
- Entry actions on arrows. Setup copied onto each incoming transition drifts as arrows are added. Move it into
entry. - Self-transition where an internal one was meant. Re-entering resets timers and counters. Draw internal transitions inside the box and test that entry did not run.
- Overlapping guards. Two transitions on the same event with guards that can both be true make behaviour depend on declaration order. Lint for it, or make the order an explicit, documented priority.
- Persisting only the leaf. With history or regions, the leaf alone cannot rebuild the machine. Persist the full configuration plus history records, and version the format so an old row loads after a state is renamed.
- Diagram drift. A hand-drawn wiki diagram stops matching the code within months. Generate it from the definition in CI.
Leadership lens
- Ask for a state machine diagram in design review for any entity with a status column. Missing arrows surface product questions (can a shipped order be cancelled?) before they become incidents.
- Make "unhandled event" behaviour a team-wide convention, then enforce it in code review and tests. Inconsistent ignore-versus-reject choices across services are a constant source of support tickets.
- Treat the definition as the source of truth and generate diagrams and tests from it, so documentation stays cheap enough to maintain.
- Adopt a statechart runtime only when hierarchy, history or regions are real requirements; for a flat lifecycle, a table and a conditional database update are cheaper to own.
Check yourself
The active configuration is
On > Active > Paused, and onlyOnhandlespower. In what order do exit actions run, and why that order?Show answer
Paused, thenActive, thenOn. Exiting inside-out guarantees a child tears down before the parent whose resources it may depend on.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.
Onhas shallow history and was last left fromActive > Paused. Re-enteringOnthrough history ends in which leaf?Show answer
Playing. Shallow history restores the direct childActive, andActivethen starts at its own initial child. Deep history would restorePaused.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
- UML 2 State Machine Diagrams (Derek Banas): the notation for states, guards, actions and composite states, drawn step by step.
- Reactive State Machines and Statecharts (David Khourshid, Uphill Conf 2019): statecharts applied to UI code, including hierarchy and history, from the author of XState.
- Creating statecharts, part 4: Composite states (Tom Mens): composite states and their entry and exit behaviour, shown in a statechart tool.
Free resources
- UML 2.5.1 specification (OMG): chapter 14 defines state machines, run-to-completion and the transition execution order precisely.
- State Chart XML (SCXML), W3C Recommendation: a standard statechart format with an executable pseudocode algorithm for exits, entries and conflicts.
- A Crash Course in UML State Machines (Miro Samek): hierarchical machines for embedded code, with the reasoning behind each semantic rule.
- Statecharts.dev: a practical glossary and guide to statecharts, with arguments for and against using them.
- State diagrams (Mermaid docs): the syntax used in this lesson, including composite states, choice and concurrency.
- UML state machine (Wikipedia): background on Harel statecharts, hierarchy, history and orthogonal regions.