State Machine Diagrams
Many objects behave differently depending on what has already happened to them. A document that has been approved cannot be sent back to draft. An order that has shipped cannot be cancelled. When an object's valid operations depend on its current state, a state machine is the clearest model — and a state machine diagram makes that model visible at a glance.
A state machine diagram shows the states an object can be in (circles or rounded rectangles), the events that trigger movement between them (labeled arrows), and the initial state (a filled dot). Reading it tells you every legal transition and, by omission, every illegal one.
A document review workflow
Consider a content-management system where documents move through an editorial pipeline before
publishing. A document starts as a draft. An author submits it for review, moving it to
in_review. A reviewer either approves it (moving to approved) or rejects it (sending it back
to draft for revision). An approved document can be published; a published document can be
archived.
Every arrow is a named event. The absence of an arrow is a constraint: there is no draft --> published transition, which means code cannot skip the review step without going through the
diagram's defined path. Drawing the diagram before writing any code makes the allowed transitions
explicit and provides a reference for the implementation.
Translating the diagram to code
The most direct translation is an enum for the states and a method for each event. Each method checks the current state, raises an error if the transition is invalid, and updates the state if it is allowed. This is the tell, don't ask principle applied to state: callers tell the document to advance; they do not read its state and branch on it themselves.
1from enum import Enum, auto
2
3class DocumentState(Enum):
4 DRAFT = auto()
5 IN_REVIEW = auto()
6 APPROVED = auto()
7 PUBLISHED = auto()
8 ARCHIVED = auto()
9
10class Document:
11 def __init__(self, title: str):
12 self._title = title
13 self._state = DocumentState.DRAFT
14
15 @property
16 def state(self) -> DocumentState:
17 return self._state
18
19 def submit(self) -> None:
20 self._require(DocumentState.DRAFT)
21 self._state = DocumentState.IN_REVIEW
22
23 def approve(self) -> None:
24 self._require(DocumentState.IN_REVIEW)
25 self._state = DocumentState.APPROVED
26
27 def reject(self) -> None:
28 self._require(DocumentState.IN_REVIEW)
29 self._state = DocumentState.DRAFT
30
31 def publish(self) -> None:
32 self._require(DocumentState.APPROVED)
33 self._state = DocumentState.PUBLISHED
34
35 def archive(self) -> None:
36 self._require(DocumentState.PUBLISHED)
37 self._state = DocumentState.ARCHIVED
38
39 def revoke(self) -> None:
40 self._require(DocumentState.APPROVED)
41 self._state = DocumentState.DRAFT
42
43 def _require(self, expected: DocumentState) -> None:
44 if self._state is not expected:
45 raise ValueError(
46 f"cannot perform this action in state {self._state.name}"
47 )