Most non-trivial software has an implicit state machine: entities (orders, jobs, sessions) move through stages, and only certain transitions are valid. A payment can be pending, then completed or failed; a job can be queued, running, or done. We don’t always draw the machine — it’s buried in if/else and flags — but it’s there. That hidden structure is the “DNA” of the system: it determines what can happen, what can’t, and what we might forget (e.g. the path from “running” to “cancelled”).
Making the state machine explicit (states, transitions, guards) pays off. You can see dead ends, missing transitions, and inconsistent flags. You can generate tests that cover every transition or every state. You can document and review the behavior in one place. Many bugs in production come from the code drifting away from the intended machine: a new state was added in one place but not another, or a transition was forgotten in an error path.
State machines don’t have to be fancy. A table (state × event → next state) or a small DSL is enough. The point is to have a single source of truth for “what states exist and what transitions are allowed.” Code then implements that; tests and tools can check the implementation against the spec. When an LLM generates code, it’s implementing (or extending) an implicit machine — if the machine were explicit, you could check the model’s output against it.
In legacy code the machine is often undocumented. You can reverse-engineer it (manual or with tooling) and then maintain it. Going forward, designing the state machine first and then writing or generating code to match it is a way to keep structure stable even when the model is additive.
Expect more tooling that extracts or checks state machines from code, and more patterns for “spec the machine, then implement.”
nJoy 😉
