Why Writing State Machine Code by Hand Is a Bad Idea
In embedded software projects, the most critical and most complex logic typically lives inside a state machine. Motor controller, communication protocol, user interface flow, power management — all of them answer the same question: "What state am I in right now, and how do I respond to each event?"
And most teams answer that question the same way: switch/case blocks, enum definitions, state variables, chains of if statements. It works. For a while.
A Familiar Scenario
Consider a vehicle door controller. The first version has four states: Open, Closing, Closed, Opening. The code is 200 lines — readable, testable.
Six months later the requirements change:
- Automatic mode added — the door should close on its own after a set period
- Manual mode added — the user can disable automatic behaviour
- Switching between the two modes must be possible at runtime
- The last state must be remembered across power failures
- A new sensor added — fully open and fully closed positions must be detected separately
The code is now 800 lines. Every switch block checks every state. The isAutomatic mode flag is evaluated in every condition. A new developer spends a week just trying to understand the code.
This is state explosion — the inevitable fate of hand-written state machines.
The Real Costs
The cost of hand-written state machine code is usually not measured accurately, because it accumulates.
Maintenance cost. Adding a new state or transition requires touching every switch block. It is easy to update one place and forget another. The compiler will not catch it.
Testing cost. Every state–event combination must be tested. 10 states × 10 events = 100 test scenarios. Writing and maintaining those by hand takes serious effort. In most projects, it simply does not happen.
Onboarding cost. When a new developer first looks at the code, there is no model — only code. They have to reverse-engineer the design intent from the implementation. They rarely succeed fully, and end up working around existing logic rather than extending it cleanly.
Synchronisation cost. Even when a design document exists, keeping it in sync with the code is a separate workload. In most projects, the two drift apart over time.
The Root of the Problem
The problem is not using switch/case. The problem is this: the model and the implementation live separately.
A designer draws a diagram. A developer translates that diagram into code. There is no automatic link between the two. The diagram changes, the code changes — independently of each other.
In hierarchical state machines this becomes even more painful. Nested states, history pseudostates, parallel regions — modelling these correctly with C++ switch/case is both difficult and error-prone.
What the Model-Based Approach Changes
UMTSM approaches this problem from a different angle: derive both the model and the code from the same source.
The developer defines the state machine in a .umt file. From that definition:
- The C/C++ state machine skeleton is generated automatically
- Test fixtures and mocks are generated automatically
- The CMake build configuration is generated automatically
When the model changes, the generated code changes. User implementations (action code) are preserved. Design and implementation never drift apart.
In the door controller example, all five requirement changes were reflected in the .umt file:
persistent deep history -> ManualMode_Open;
state AutomaticMode
{
state Open
{
entry / resetWaitingTime;
do / wait; // ← timer thread
-> Closing; // ← automatic transition on completion
Manual -> ManualMode:Open; // ← single-line mode switch
}
}
These seven lines express:
- When entering the
Openstate, the timer is reset do / waitruns as a thread and completes when the time expires- On completion, a transition to
Closingis taken - When the
Manualevent arrives, a transition toManualModeis taken
The generated C++ code implements this semantics correctly and deterministically — including mutex protection, thread management, and history restoration.
"But We Already Have Working Code"
Most projects reach this point and say "the migration is not worth it." I understand — changing working code carries risk.
But the right question is: what is the maintenance cost of the existing code?
How long does each new feature take? How many weeks does it take to onboard a new developer? Is the test coverage genuinely sufficient? How many files does a single requirement change touch?
The answers to these questions usually reveal the true cost of hand-written state machine code that has no model behind it.
Conclusion
Writing state machine code by hand is not wrong — but it does not scale. As the system grows, requirements change, and the team expands, the gap between model and implementation does not close; it widens.
The value of the model-based approach comes from this: design intent lives in the source, testability is structurally guaranteed, and a new developer can understand the system by reading the .umt file.
UMTSM was developed to bring this approach to embedded C/C++ projects. In the next post, we look at how the generated code is tested and how the automatically generated test fixtures are used.
For questions or projects related to UMTSM, feel free to get in touch.
