Register your interest: Tag @Cody, get an agent
BlogEngineering

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

Naming the states and the legal transitions between them turns implicit rules into something checkable. Where status columns go wrong, and when the formality is worth it.

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 says a thing can be in one of a defined set of states, and that only certain transitions between them 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 one. The question is whether it is written down and enforced, or whether it exists as an understanding distributed across the code that happens to set the column — because the second version is where the impossible states come from.

What we'll cover

Every status column is one already

The states exist — they are the values the column can hold.

The transitions exist too, as an understanding of which changes make sense, held in people's heads.

Nothing enforces them. Any code with write access can set the column to anything, including a value that makes no sense from where the record currently is.

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

Nobody can answer what the states are without reading every site that writes the column, which is the practical symptom of the implicit version.

Making it explicit is not adding a state machine. It is writing down the one you already have, and discovering in the process that it has more states and fewer rules than anybody thought.

What making it explicit buys

Illegal transitions become impossible rather than merely unlikely, which removes a category of bug entirely.

The rules live in one place, so changing them is one change rather than a search.

The diagram is documentation that stays accurate, because it is the thing being enforced rather than a drawing of intent.

Testing becomes tractable. A defined set of states and transitions is enumerable, so the tests cover the model rather than sampling it.

Onboarding is faster. "Here is how an order moves" is a picture rather than an archaeology exercise.

Bugs surface at the write, where the context is, rather than downstream where a record in an impossible state causes something inexplicable.

Choosing the states

The modelling decision, and the one that determines whether this helps.

Name them for the business meaning, not for the technical situation. "Awaiting payment" rather than "step two".

Keep the set small. More than about a dozen usually means two concerns are being tracked in one column.

One concern per machine. Payment status and fulfilment status are different machines; combining them produces a combinatorial explosion of states like "paid and partially shipped and refund pending".

Include the terminal states explicitly, and be clear about which they are, since "can this still change" is a question everything downstream asks.

Beware the state that means nothing, such as a default applied at creation that no code ever transitions away from deliberately.

Ask what decisions depend on the state. If no behaviour differs between two states, they are one state with two names.

Transitions, and who is allowed

Define the legal transitions explicitly, as pairs, rather than as a general permission to set the column.

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

Some transitions need conditions. Paid to shipped may require stock, an address, and a payment that actually settled, and those checks belong at the transition rather than scattered.

Who may perform it matters too. A customer may cancel; only an administrator may reinstate. Attaching authorisation to the transition puts it where the decision is.

Record who transitioned and when. The history of a record's states is frequently more useful than its current one, and it is nearly free to keep.

Reject illegal transitions loudly, with an error naming the current state and the attempted one, because a silent refusal produces a caller that believes it succeeded.

The impossible state

The bug class this prevents, and it is worth being specific about what it looks like.

A record in a state its own history could not produce — shipped without ever being paid, cancelled and then delivered.

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

A state the current code does not recognise, left by a version that has since been removed, which every long-lived system accumulates.

The downstream symptom is inexplicable. Something fails or behaves oddly because it received a record that should not exist, and the investigation starts far from the cause.

Query for them. A periodic check for records in impossible combinations is a short query and it reliably finds things — including, usually, one path nobody knew about.

Side effects on transition

Where state machines meet the real world, and where the design needs care.

Transitions trigger things. Moving to shipped sends an email; moving to cancelled issues a refund.

The transition and the side effect are not atomic. The state can change and the email fail, or the email send and the state change fail — which is the dual write problem in a different costume.

Record the state first, then act, with the action driven from the recorded change rather than from the same code path. Acting first risks a side effect for a transition that did not happen.

Make the side effects idempotent, because the driving mechanism will occasionally repeat.

Do not put business logic in the transition itself. A transition that decides whether it should happen is harder to test than one that checks preconditions and applies.

Keep an eye on the ones that cannot be undone. A transition sending a customer email is not reversible, so the checks belong before it rather than after.

When the formality is not worth it

Two states. A boolean is a state machine and formalising it is ceremony.

When nothing depends on the transitions. A status used only for display, with no behaviour attached, gains little.

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

When the states genuinely are open-ended, such as a user-defined set of categories, where enforcement would fight the requirement.

When it would be fought. 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 worth applying: are there transitions that would be wrong? If no transition is invalid, you have a label rather than a state machine.

Changing the model on live data

Adding or removing a state is a migration, and it is the part that gets underestimated.

Existing records are in old states. Removing a state means deciding what happens to everything currently in it, which is a data question rather than a code one.

Adding a state in the middle changes what the transitions mean for records already past that point, and a record that skipped it may now look invalid.

Renaming is a migration too, since the value is stored, and a rename with no data change leaves records in a state the new code does not recognise.

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

Validate after migrating, by querying for records in states the model no longer contains.

Keep the old states readable for longer than feels necessary, because a report or an export written months ago will still reference them.

Modelling processes with CodeWords

CodeWords connects to more than 3,000 integrations, and a process with defined states and transitions is described in plain language rather than 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 that 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 covers light use, with Pro at $39 per month and Business at $100 per month as usage grows; details are on the pricing page.

What to get right first

Write down the states and the legal transitions for whatever you already have, before changing any code. The document takes an afternoon, it is useful regardless of whether you formalise anything, and producing it reliably surfaces states nobody remembered and transitions two parts of the system disagree about.

Two habits make the difference. Split concerns into separate machines rather than combining them, since one column tracking payment and fulfilment produces a state list that grows multiplicatively and stops being readable. And keep the transition history rather than 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?

If you have a status column, yes — with states that are the column's values and transitions that exist as an understanding in people's heads. Making it explicit is writing down what exists rather than introducing something new.

How many states should there be?

Usually under a dozen. More than that generally means two concerns are being tracked in one column, and splitting them into separate machines is what stops the state list growing multiplicatively.

What is an impossible state?

A record in a condition its own history could not produce — shipped without being paid, cancelled with a delivery date. They come from separate code paths writing independently, and a periodic query for them reliably finds things.

Should transitions trigger side effects?

Record the transition first and drive the side effect from the recorded change, rather than doing both in one code path. The two are not atomic, so acting first risks a side effect for a transition that did not commit.

What should happen on an illegal transition?

An error naming the current state and the attempted one. A silent refusal leaves the caller believing it succeeded, which produces a downstream bug far from the cause.

Is this worth doing for a simple process?

Not for two states, and not where no transition would be wrong — that is a label rather than a state machine. The test is whether there are transitions that should be refused.

Why keep the transition history?

Because "how did this record get into this state" is where every investigation starts, and it is nearly free to record. The history is frequently more useful than the current value.

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 — and query afterwards for records left in states the model no longer contains.

Is renaming a state safe?

No, it 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 recognise.

Get started today

Your first workflow is free to build.

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