A state machine diagram shows the states one object can be in, the events that move it between states, and the conditions on those moves. Developers, testers and product owners read it whenever something has a lifecycle: an order, a ticket, a connection, a model deployment.
Many bugs come from an object doing something in the wrong state: shipping an unpaid order, refunding twice, retrying a call against a dead service. A state machine makes the allowed states explicit and lists every legal transition. Anything not drawn is forbidden.
UML state machines descend from David Harel's statecharts (1987), which added nesting and concurrency to classic finite-state machines so that real systems stay readable. The same model is used by libraries such as XState (JavaScript), the Python transitions package, and AWS Step Functions-style workflows.
A filled dot is the initial pseudostate; a bullseye is the final state. States are rounded rectangles. A transition arrow is labelled event [guard] / effect: when the event arrives and the guard is true, the effect runs and the object moves. All three parts are optional. A composite state contains its own sub-machine; a transition leaving the composite boundary applies to every substate inside. States may declare entry, do and exit activities. A diamond is a choice point.
Walkthrough: an order starts in Created, moves to AwaitingPayment on submit, and either enters the Fulfilment composite state on paymentOk or is cancelled after a 30-minute timeout. Inside Fulfilment it goes Paid → Shipped → Delivered. The highlighted rule is the guarded transition cancel [not shipped] / refund(): cancelling is only possible from Paid, and it triggers a refund. Each arrow becomes a test case, and each missing arrow becomes a negative test.
Walkthrough: a client calling an LLM API wraps calls in a circuit breaker. In Closed, calls pass and failures are counted (a self-transition). The fifth failure trips it to Open, where calls fail immediately. After 30 seconds it moves to HalfOpen and allows one trial call: success resets to Closed, failure goes back to Open. Three states and five transitions capture the whole pattern precisely.
For each state, ask two questions: which events can arrive here, and what should happen for each? Any event without an arrow is either ignored or a bug; decide which and document it. Then check that every state can reach a final state, otherwise objects can get stuck forever.
Common mistakes: naming states after actions ("Ship") instead of conditions ("Shipped"); turning the diagram into a flowchart where states are really steps; and burying business rules in long guard expressions. Keep guards short and name them after the rule ([withinReturnWindow]).
Mermaid's stateDiagram-v2 renders natively in GitHub, GitLab and many docs tools:
stateDiagram-v2
[*] --> Created
Created --> AwaitingPayment : submit
AwaitingPayment --> Fulfilment : paymentOk
AwaitingPayment --> Cancelled : timeout [after 30 min]
state Fulfilment {
[*] --> Paid
Paid --> Shipped : dispatch
Shipped --> Delivered : signed
}
Paid --> Cancelled : cancel [not shipped] / refund()
Delivered --> [*]
Cancelled --> [*]PlantUML uses nearly identical syntax inside @startuml ... @enduml.