Why Use State Machines?
A state machine describes an application as a small set of states it can be in, and the events that move it from one state to another. I build applications this way; here is why.
- The application is conceptualized as states and transitions between states.
- These states and transitions are declared in a map.
- A map is declarative. You can see it and reason about the application visually.
- When we say a map is declarative, it means the code describes “what” the system should do, not “how” to do it step-by-step. Because the structure is purely data it acts as a static blueprint.
- The application is no longer a bunch of code determining what the application is doing now (imperative).
- A map is declarative. You can see it and reason about the application visually.
- The state machine abstraction allows you to reduce complex code to a concise map.
- The flow of the application becomes explicitly deterministic: every allowed state change is declared in the map.
- The mental model does not have a lot of parts:
States: What the Application Is Doing Now
- The different states that the application can be in. A state machine is always in only one state. Examples:
- Getting user input
- Fetching AI reply
- Fetching database records
- Saving result
- Calculating profit
Events: Cause a Transition to the Next State
- The triggers that tell the machine it is time to transition to a new state:
- a user clicks a button to send a prompt to an AI
- a user clicks a menu item to copy text to the clipboard
- a reply is received from the AI
- data is fetched from a database
- data is saved to the database
- calculation of profit is completed
Guards: Make Sure the Transition Is Allowed
- Logical conditions evaluated before a transition is taken. They act as a “lookahead” gate; if the guard returns False, the transition is blocked. [Only one guard per transition]
- Guards never perform actions or mutate data. They are pure, true/false evaluations that inspect the state machine’s context and event payload right after an event arrives, but before any state changes.
- Check that the prompt is valid before sending it to the AI.
- Confirm the user has admin privileges before allowing deletion of important data.
Actions: Do the Work
- “Muscles”. Where the work gets done.
- Entry actions: fire automatically upon entering the state.
- Send the prompt to the AI upon entering the state “Waiting for AI Reply”
- Tell the user interface to dim and show a “loading spinner” when entering the state “Fetching records from database”
- Exit actions: fire automatically before leaving the state.
- Tell the UI to hide the “loading spinner” when exiting the state “Fetching records from database”
- Clear the form fields when exiting the state “Get user information”
- Transition actions: located on the transition line, they fire only when moving between states.
- When transitioning from the state “Calculating Profit” to the state “Save Result” because the event “Calculation Done” fired, take the computed values out of the state machine context and hand them off to the Database Access Layer to be permanently written to the disk.
More Benefits
- A state map can be used with multiple user interfaces.
- The only code that gets changed when moving an app from desktop to phone or web is the widget layer (user interface) and the layer just below it, the bridge which translates widget clicks into events. The state map is not changed. Testing of the state map benefits all the implementations.
- It makes the flow predictable.
- When you build an application using imperative code, it is very easy for the code to fall into a broken state. We call this implicit state, and it is where most software bugs come from.
- By using an explicit state map in application development, you trade chaotic, hard-to-predict code for far more control. We can prove things like:
- Specific events are only handled in specific states.
- A given state can not be reached erroneously.
- Systematic testing
- Testing a regular application usually involves guessing all the weird ways a user might break it.
- With a state machine, you have a complete map of every path through your application. That makes it possible to write automated tests that systematically traverse states, transitions, guards, and actions, proving coverage of the topology instead of guessing which user paths might matter.
- The user interface (buttons, menus, dialogs, forms) automatically mirrors the exact capabilities of the underlying state machine. That is a big architectural win over the imperative code model.
- Buttons and menu items are enabled by event legality. Each button or menu item is bound to an event. When that event is legal from the current state, the UI element is enabled. When the event is not legal from the current state, it is disabled. The UI does not decide this with custom code; it mirrors the state machine.
- Forms and dialogs are bound to states. Entering the state activates the form or dialog. Leaving the state deactivates it.
- From the developer’s perspective these are handled automatically simply by mapping the UI elements to the events or states (one declaration each in a database). This removes a large chunk of complex code in an imperative code application and makes it easy to think about.
Thinking About the Application Differently
Some will say: Imperative code is deterministic as well, right?
Yes. Technically, all standard imperative code is completely deterministic under the hood. A computer chip will always execute the line of code you wrote exactly the same way every time.
But there is a catch.
Imperative code is micro-deterministic (at the line-by-line level), but it quickly becomes macro-chaotic (at the application level). A state machine solves this by making that determinism explicit, visible, and manageable.
Here is the difference between how imperative code hides its determinism and how a state machine makes it obvious:
1. Imperative Code Hides the Map
In an imperative application, the “state” of your app is smeared across dozens of different variables scattered throughout your code (is_loading, user_authorized, current_page_index, api_error_count).
- The Problem: The deterministic rules of your application are buried inside a messy maze of nested if statements. To figure out what the app will do when a button is clicked, a human developer has to mentally simulate the computer running that code and keeping track of all the variables and branches.
- The Result: The code is technically deterministic, but it is complex and hard to understand and debug.
2. A State Machine Externalizes the Map
A state machine takes all those hidden variables and condenses them into a single, unified blueprint—the state map.
Instead of asking, “What happens if is_loading is true, but user_authorized is false, and the network drops?” you simply look at the map and see if there is a line connecting the Loading state to the NetworkError state.
- The Advantage: The rules aren’t hidden inside the behavior of functions; they are defined upfront as a data structure. You can print the map, look at it, and see every single permitted path.
The Analogy: A Jungle vs. A Train Track
Think of the difference like this:
- Imperative code is a dense jungle: There are paths everywhere, hidden traps, and dense trees. You can technically walk through it deterministically if you know the exact steps, but it is very easy to get lost, hit a dead end, or step on a hidden trap (commonly known as a bug in coding).
- A state machine is a train track: The tracks are not movable. A train can only go exactly where the rails lead. If there is no track leading from Station A to Station C, the train cannot go there.
The state machine engine forces your application to run on tracks. Imperative code is still doing the work inside the actions, but the map decides when those actions are allowed to run.
In imperative code, it is not just that the state variables are scattered throughout the code. The added problem is that the history of how you got there is completely hidden and it is complex.
Branching History
When an imperative application crashes, looking at the variables at the exact moment of the crash only tells you where you are. It doesn’t tell you the path you took to get there. So we add logging. It is complex because the application is a complex flow and it’s easy to leave out logging details that are needed.
- Imperative Chaos: Did the Save button stay disabled because validation failed, because the record was already saving, because a stale async callback overwrote the form state, or because a previous error flag was never cleared? At the moment of failure, the variables might only say can_save = false, but the code path that made it false could be hidden across event handlers, callbacks, timers, and UI state updates.
- The Log: Because that path history is missing, you cannot just look at a variable dump. You need a log. Because the flow is complex, the logging has to be too, and it is easy to leave out the detail you later need.
State maps make runtime history much easier to log because the flow is already structured. A useful replay log needs the following items for each advance through the map.
- the event
- the event payload
- the transition taken
- guard results
- action results
- context changes
- emitted feedback/status events
- the destination state
With the map plus this ordered trace, a tool can visually replay the application history: highlight each state, animate each transition, show guards and actions, and display context/payload changes. Debugging becomes following the declared rails instead of reconstructing branching history from scattered imperative code.
The Three Great Advantages of State Machines
1. Spatial Reasonability (The Power of “Seeing”)
Imperative code is temporal—it exists as lines of execution moving forward through time. You can only understand it by reading from top to bottom and simulating the clock cycles in your head.
A declarative map is spatial. It maps your entire application out like a physical territory. Because it is static data, you can look at the whole system at once. A human can trace paths with a finger and visually verify: “Yes, there is a track connecting the loading screen to the error screen, so that flow is covered.”
2. Eliminating the “Execution Gap”
In an imperative application, there is always a gap between what the developer thinks the code does and what the computer actually executes. Hidden edge cases live in that gap.
With a state map, the data structure is the execution runtime. There is no gap. If a state transition line isn’t explicitly defined in your map the engine cannot execute it. You can confidently reason about the application visually because the visualization and the runtime logic are the exact same thing.
3. Anyone Can Read It
Think of your declarative map like an architectural blueprint for a house:
- Declarative (Your Map): The blueprint clearly shows a living room, a hallway, and a kitchen. You can instantly see how they connect. You know you can walk from the kitchen to the living room because the hallway is drawn right there.
- Imperative (Standard Code): Instead of a blueprint, you are given a 500-page book of strict, line-by-line walking directions: “Take three steps forward, turn 90 degrees left, check if the floorboards are wet, if true take one step back…” You have no idea what the house looks like until you blindly walk through it and hope you don’t hit a wall.
By shifting your application architecture into a map, you have stopped writing walking directions and drawn a blueprint.
The New Bottleneck
A lot of headlines are out there these days saying things like “AI made writing code cheap, but verifying it correct/safe is now the bottleneck”. AIs make these state maps and applications extremely well.
In my experience using this framework with AIs to make applications, the verification process has changed.
This framework moves a huge amount of verification out of ad hoc code review and into architecture.
The AI is not being asked to invent arbitrary program flow in raw Python. It is being asked to fill in constrained slots:
- declare states, events and transitions in the single source of truth (SOT), the database where the map lives
- bind buttons and forms to events
- write small atomic actions
- write Database Access Layer (DAL) methods behind a known boundary
- write typed vessels (the typed data containers that carry values between layers)
- obey the layer rules (what each layer is allowed to call)
- pass the automatic checks (the exporter and linter), which reject code that breaks the rules
That makes AI-generated work much easier to trust because the system constrains the shape of possible mistakes.
The verification burden does not disappear. It changes form:
- less “read 5,000 lines and infer behavior”
- more “check the map, enforce the gates, traverse the topology, validate boundary contracts”
That is a much better environment for AI because AI is weakest when it must maintain hidden global consistency across scattered imperative code. This framework gives it rails: the linter, database schema, state machine topology, typed context, and generated contracts all narrow the blast radius.
This system is designed so verification is structural, automatic, and local: structural because it checks code shape and boundaries; automatic because many checks run mechanically from the declared structure; and local because each behavior is checked at the map row, contract, type, or layer boundary that owns it, instead of by tracing the whole program.