A recurring architectural mistake — and the state machine pattern that fixes it.
A pattern you've probably seen before
There's a particular kind of mess that shows up in almost every system that models a multi-stage business process: quoting, onboarding, hiring, procurement, claims, and bookings. It starts innocently. Someone models the first stage as an entity. Then the second stage needs its own fields, so it becomes its own table. Then the third stage needs different permissions, different actors, different data — another table. A few years in, you have four or five entities, each representing what is, to the actual user, a single continuous journey.
Nobody designed it that way on purpose. It accreted, one "just add a table for this stage" decision at a time.
And the cost isn't abstract. It shows up as:
- Multiple state machines for one process. Each entity has its own status field, its own transition rules, its own edge cases — even though the business only has one journey in mind.
- Duplicated data. The same facts get copied from one entity to the next as a record moves through the pipeline, because nobody wants to join across five tables to answer "what's the current status of this thing."
- Manual synchronization. Every time a field needs to be updated, someone has to remember to update it in two, three, or four places. This is the single most common source of "why does the UI show something different from the database" bugs.
- Ambiguous ownership. When a bug report comes in, the first ten minutes are spent figuring out which of the four tables is actually authoritative right now.
If any of that sounds familiar, the root cause is almost always the same thing: the entity you modeled first isn't the entity that actually persists.
Find the thing that doesn't expire
Every one of these fragmented systems has a durable anchor hiding underneath it — a customer, an asset, an account, a physical thing — that outlives every individual "process" instance running against it. But because the first stage of the process got modeled first, that transient process — not the durable thing — ended up as the root entity.
The fix starts with a genuinely simple question: what still exists after this process ends?
- In a sales pipeline, it's not the "opportunity" — it's the account.
- In an onboarding flow, it's not the "application" — it's the person or organization.
- In a procurement system, it's not the "purchase request" — it's the vendor relationship or the asset being purchased.
Once you identify that durable anchor, everything else — every stage of the process — becomes something that happens to the anchor, not something the anchor is subordinate to. That single reframing does more architectural work than almost any other decision you'll make on the project.
Separate what's true from what's negotiated
The second useful split is between static facts and dynamic process state.
Static facts describe reality independent of any particular transaction: the specifications, the requirements, the physical or structural details. These don't care which vendor, which buyer, or which negotiation is currently in progress — they're just true.
Dynamic process state is everything specific to a given transaction: pricing, timelines, who proposed what, what's been agreed to so far, and what stage things are in. This is the part that changes shape as a deal moves forward, and — importantly — this is the part that's allowed to have multiple concurrent instances (multiple vendors bidding on the same static facts, for example) without any of them corrupting the underlying truth.
Keeping these separate means the "truth" layer stays stable and shared, while the "in-progress deal" layer can be duplicated, versioned, and negotiated freely without ever touching the facts underneath it.
Collapse the process into one state machine
This is where the actual consolidation happens. Instead of a new table for each stage of the process, you model the entire journey — from first draft to final close-out — as one aggregate governed by one state machine.
A few properties make this work well in practice:
- States, not tables, represent stages. Draft, negotiation, agreement, execution — these are values of a status field, not separate schemas with separate foreign keys pointing at each other.
- Transitions are explicit and guarded. Moving from one state to the next requires specific conditions to be met (required fields present, an approval given, a signature captured) and triggers specific side effects (notifications, snapshots, downstream updates). Nothing moves silently.
- Terminal states don't spawn new entities. The most common failure mode in the old pattern is treating "the deal is signed" as the boundary where ownership transfers to a completely different system. It doesn't have to. Whatever happens next — fulfillment, execution, delivery — can continue as further states and transitions on the same record.
The result: one thing to query for "what's the current state of this," one thing to authorize against, one history to audit.
Make history a feature, not an accident
Consolidating stages into one aggregate raises an obvious question: if everything lives in one place, how do you avoid losing the trail of who-agreed-to-what-when, especially once something becomes contractually binding?
The answer is to never edit in place. Every change — a price adjustment, a scope tweak, a revised timeline — creates a new, timestamped version of the record rather than overwriting the old one. "Current state" is just whichever version hasn't been superseded. Nothing is ever destroyed; it's layered.
This has two payoffs that matter more than they first appear:
- Auditability becomes free. You're not reconstructing history from logs or guessing based on timestamps scattered across tables — the history is the data model.
- Locking becomes cheap and safe. Once a record reaches a binding state, you simply stop accepting new versions against it (or route further changes through an explicit amendment process). You don't need special-case logic to "freeze" a table, because immutability was the default all along.
Why does this increase flexibility, not just reduce complexity
It's tempting to file this under "cleanup" — fewer tables, less duplication, easier debugging. That's true, but it undersells the bigger payoff: a single, well-defined state machine is dramatically easier to extend than four loosely coordinated ones.
Adding a new stage to a fragmented system means designing a new table, new foreign keys, new sync logic, and new permission rules that have to stay consistent with everything upstream. Adding a new stage to a unified state machine means adding a new state and a new transition. The blast radius of a change shrinks from "the whole pipeline" to "one edge in a graph."
That's the real argument for this pattern. It's not just fewer moving parts — it's a system that can absorb new requirements without anyone needing to reason about four different lifecycles at once.
The takeaway
This isn't specific to any one domain. Anywhere a multi-stage business process has been modeled as a chain of separate entities instead of one evolving record, the same three moves tend to apply:
- Find the entity that actually persists, and anchor everything to it.
- Separate static truth from negotiable, in-progress state.
- Represent the entire process as one state machine with explicit, guarded transitions — and let history accumulate through versioning instead of in-place edits.
None of this requires exotic tooling. It requires being willing to ask, early, whether the entity you're about to model is the one that will still matter next year — or just the first stage of a process that got mistaken for the whole system.