BlogEngineering

What is a state machine, and why does a status column become one badly?

A state machine names every state and legal transition. See how status columns go wrong and when making it explicit pays off.

Isha MagguIsha Maggu11 min read

Summarize with AI

What is a state machine, and why does a status column become one badly?
On this page

A state machine is a model in which a record is in exactly one of a defined set of states, and only certain transitions between states are permitted. An order is pending, paid, shipped, or cancelled; paid can become shipped; shipped cannot become pending.

Every system with a status column already has a state machine. The question is whether the rules are written down and enforced, or scattered across the code that sets the column. The scattered version is where impossible states come from.

What we'll cover

Key takeaways

  • Any system with a status column already has a state machine, so the work is writing down the rules you have.
  • An explicit model makes illegal transitions impossible instead of unlikely.
  • Keep one concern per machine and name states for their business meaning.
  • Record the transition first, then trigger side effects from the recorded change.
  • Treat adding, removing, or renaming a state as a data migration.

Every status column is one already

Every status column is a state machine with unwritten rules. Five properties show why the implicit version fails.

The states exist. They are the values the column can hold.

The transitions exist as an understanding. People hold in their heads which changes make sense.

Nothing enforces the transitions. Any code with write access can set the column to any value, including one that makes no sense for the record's current state.

The rules are scattered. Each place that sets the status has its own idea of when a change is appropriate, and the ideas drift apart.

Nobody can list the states without reading every place that writes the column.

Making the model explicit means writing down the one you already have. The process usually reveals more states and fewer rules than anyone thought.

What making it explicit buys

An explicit state machine makes illegal transitions impossible, keeps the rules in one place, and gives you documentation that stays accurate. Six benefits follow.

Illegal transitions become impossible instead of unlikely, which removes a category of bug.

The rules live in one place, so changing them is one change instead of a search.

The diagram is documentation that stays accurate, because the diagram is the thing being enforced.

Testing becomes tractable. A defined set of states and transitions can be listed in full, so tests cover the model instead of sampling it.

Onboarding is faster. "Here is how an order moves" is a picture, not an investigation of the code.

Bugs appear at the write, where the context is, instead of downstream where an impossible record causes something inexplicable.

Choosing the states

Choose states by business meaning, keep the set small, and track one concern per machine. This modelling decision determines whether the machine helps.

Name states for the business meaning, not the technical situation. Use "awaiting payment" instead of "step two".

Keep the set small. A set larger than about a dozen usually means two concerns share one column.

Use one concern per machine. Payment status and fulfilment status are different machines. Combining them produces states such as "paid and partially shipped and refund pending".

Name the terminal states explicitly, because everything downstream asks whether a record can still change.

Remove states that mean nothing, such as a default applied at creation that no code ever transitions away from on purpose.

Ask which decisions depend on the state. If two states lead to the same behaviour, they are one state with two names.

Transitions, and who is allowed

Define legal transitions as explicit pairs, with the conditions and the permitted actor attached to each. A general permission to set the column defeats the purpose.

Define the legal transitions as pairs (from state, to state).

Not every transition is symmetric. Cancelled to pending is usually illegal even though pending to cancelled is fine. An implicit model permits both.

Some transitions need conditions. Paid to shipped may require stock, an address, and a settled payment. Put those checks at the transition, not scattered through the code.

Attach authorization to the transition. A customer may cancel; only an administrator may reinstate.

Record who made each transition and when. The history of a record's states is often more useful than its current state, and keeping it costs little.

Reject illegal transitions loudly, with an error naming the current state and the attempted one. A silent refusal leaves the caller believing the change succeeded.

The impossible state

An impossible state is a record in a condition its own history could not produce. Four forms are common.

A history that could not happen. Shipped without ever being paid, or cancelled and then delivered.

Two fields that disagree. A status of cancelled with a fulfilment date, because two code paths wrote independently.

An unrecognized state. A removed version of the code left a value that the current code does not know. Every long-lived system accumulates these.

A distant symptom. Something downstream fails because it received a record that should not exist, and the investigation starts far from the cause.

Query for impossible states on a schedule. A short query for records in impossible combinations usually finds at least one code path nobody knew about.

Side effects on transition

Transitions trigger real-world actions, and the transition and its side effect are not atomic. Moving to shipped sends an email; moving to cancelled issues a refund.

The state can change and the action can fail, or the action can run and the state change can fail. This problem is called the dual write problem.

Record the state first, then act. Drive the action from the recorded change, not from the same code path. Acting first risks a side effect for a transition that never happened.

Make side effects idempotent (safe to run twice), because the driving mechanism will occasionally repeat.

Keep business logic out of the transition. A transition that decides whether it should happen is harder to test than one that checks preconditions and applies the change.

Put checks before irreversible actions. A customer email cannot be unsent, so validate before it, not after.

When the formality is not worth it

Skip the formal model when nothing depends on the transitions. Five cases apply.

Two states. A boolean is a state machine, and formalizing it adds ceremony.

Display-only status. A status with no behavior attached gains little.

Short-lived records that are created, processed, and discarded within one request.

Open-ended states, such as user-defined categories, where enforcement would conflict with the requirement.

A team that rejects the model. A model imposed on a team that does not agree with it gets worked around, and the workarounds are worse than the implicit version.

The test to apply: are there transitions that would be wrong? If no transition is invalid, you have a label, not a state machine.

Changing the model on live data

Adding, removing, or renaming a state is a data migration, and teams underestimate it.

Existing records sit in old states. Removing a state means deciding what happens to every record currently in it. That is a data question, not a code question.

Adding a state in the middle changes what transitions mean for records already past that point. A record that skipped the new state can look invalid.

Renaming is a migration, because the value is stored on every record. A rename with no data change leaves records in a state the new code does not recognize.

Deploy the reader before the writer. Code that understands both old and new states, deployed first, avoids a window where something cannot interpret what it reads.

Validate after migrating. Query for records in states the model no longer contains.

Keep old states readable longer than feels necessary, because a report or export written months ago still references them.

Modelling processes with CodeWords

CodeWords connects to 3,000+ integrations (CodeWords pricing page, 2026), and a process with defined states and transitions can be described in plain language instead of implemented as a machine you maintain.

  1. Open CodeWords and start a new automation.
  2. Describe the process to Cody, the automation builder: the states, what moves between them, and what each transition should cause.
  3. Describe the exceptions: a transition that should be refused, a record already in the target state, a side effect that fails.
  4. Connect the systems involved once and reuse the connections.

You describe the outcome; Cody builds it, connects it, and deploys it. The Free plan costs $0, Pro costs $39 per month, and Business costs $100 per month (CodeWords pricing page, 2026). Details are on the pricing page.

What to get right first

Write down the states and legal transitions for the process you already have, before changing any code. The document takes an afternoon, helps even if you never formalize anything, and reliably surfaces states nobody remembered and transitions where two parts of the system disagree.

Two habits make the difference. Split concerns into separate machines, because one column tracking payment and fulfilment produces a state list that grows multiplicatively. And keep the transition history, not only the current state, because "how did this record get here" is the question every investigation starts with.

Frequently asked questions

Do I already have a state machine?

Yes, if you have a status column. The states are the column's values, and the transitions exist as an understanding in people's heads. Making the machine explicit means writing down what already exists, not adding something new, and the exercise usually reveals states nobody remembered.

How many states should there be?

Usually fewer than a dozen. A larger set generally means two concerns are tracked in one column. Splitting them into separate machines stops the state list from growing multiplicatively, and each machine stays small enough to read and test in full.

What is an impossible state?

An impossible state is a record in a condition its own history could not produce, such as shipped without being paid, or cancelled with a delivery date. They come from separate code paths writing independently, and a periodic query for them reliably finds problems.

Should transitions trigger side effects?

Yes, but record the transition first and drive the side effect from the recorded change, not from the same code path. The two are not atomic, so acting first risks a side effect, such as a customer email, for a transition that never committed.

What should happen on an illegal transition?

Return an error that names the current state and the attempted state. A silent refusal leaves the caller believing the change succeeded, which produces a bug far from its cause. A loud error makes the faulty code path easy to find.

Is this worth doing for a simple process?

No, not for two states, and not where no transition would be wrong. In those cases the status is a label, not a state machine. The test is whether any transition should be refused. If one should, an explicit model pays off.

Why keep the transition history?

Keep it because "how did this record reach this state" is where every investigation starts, and recording it costs little. The history is often more useful than the current value, and it supports audits, debugging, and reports on how long records stay in each state.

How do I add or remove a state safely?

Treat it as a migration. Deploy code that understands both old and new states first, migrate the data, then remove the old handling. Afterward, query for records left in states the model no longer contains and fix them before closing the change.

Is renaming a state safe?

No, renaming is a migration, because the value is stored on every record. A rename deployed without a data change leaves existing records in a state the new code does not recognize, so update the stored values in the same release.

Get started today

Your first workflow is free to build.

Describe what you need. Cody handles the build, the connections, and the deployment.