Order states versus order flags: the distinction that keeps a transaction consistent
In short
An order state is a value that changes which transitions are legal from where the order stands. Everything else — disputed, reviewed, flagged for moderation — is an attribute of the record, not a state of the machine. Apply that one admission test and most status enums shrink to 5 or 6 values, while the fields inflating them turn out to belong to a different lifecycle.
Key takeaways
- The admission test is one question: does this value change which transitions are legal from here? If not, it is a flag.
- Flags change who may act and what the interface shows. States change what may happen next. Conflating them produces status names with 'and' in them.
- A status enum growing by multiplication rather than addition means 2 lifecycles have been merged into 1 column.
- Refunded belongs to the payment, disputed belongs to the dispute record, reviewed is a derived existence check. None of the 3 is an order state.
- Every transition must record actor, instant and reason, or the state column cannot answer the only questions operations ever asks.
There is one admission test for the order state machine, and it takes a sentence to apply: does this value change which transitions are legal from here? If it does, it is a state. If it only changes who may act, what the interface shows, or what a report counts, it is a flag on the record — a column, not a value in the status enum. Almost every argument about order modelling in a marketplace is this distinction, unexamined.
Get it wrong in one direction and the enum explodes: paid_disputed, paid_disputed_refunded, fulfilled_flagged, each needing its own transitions and its own tests. Get it wrong in the other and the machine loses its guarantees, because a value that genuinely constrains the next step sits in a boolean nobody validates against. The tell for the first is a status name containing the word "and"; the tell for the second is a comment explaining that this transition is only allowed when some flag is false.
The admission test, run over six fields teams argue about
| Field | Verdict | Why |
|---|---|---|
| paid | State | It changes the legal set: fulfilment becomes available, and cancellation without a refund path stops being available |
| seller_accepted | State, but on the seller's lifecycle | It gates fulfilment in an accept-first market. Merged into the same column as paid, it multiplies the enum instead of extending it |
| refunded | Neither — it is a state of the payment | An order can be partly refunded, refunded twice or refunded after completion. None of those has a coherent position in an order enum |
| disputed | Flag on the order, state on the dispute | The dispute has its own lifecycle and its own record. On the order it changes who may act, not what may happen next |
| reviewed | Derived, not stored | It is an existence check against the review table. Storing it creates a second truth that drifts the first time a review is deleted |
| flagged_for_moderation | Flag | It suspends actors rather than transitions. An operator can still move the order; a seller cannot |
The reviewed row is worth pausing on, because it is the one teams add without thinking. Whether a review exists is a fact about another table, and a review is in any case an input to something else entirely — the separation between a raw signal, a computed score and a sort order is in signal, score and sort: three things people conflate. Copying it onto the order buys a faster query and pays for it with a consistency problem.
The transition table, with the illegal cells marked
The states that survive the test are few enough to write out in full, and writing them out in full is the point: a transition table is the artefact that turns an argument about wording into a review of specific cells. This one is for an accept-then-pay market, where a seller confirms before money moves.
| From \ To | accepted | paid | fulfilled | cancelled | completed |
|---|---|---|---|---|---|
| created | Legal — seller confirms | Illegal — nothing has been agreed to pay for | Illegal | Legal — buyer withdraws, or the hold expires | Illegal |
| accepted | — | Legal — buyer pays | Illegal — unpaid fulfilment is a policy exception, not a transition | Legal — either party, while no money has moved | Illegal |
| paid | Illegal — acceptance cannot follow payment | — | Legal — seller delivers | Legal, but only through the refund path | Illegal — fulfilment cannot be skipped |
| fulfilled | Illegal | Illegal | — | Illegal — a return is a different transition with different money | Legal — on buyer confirmation or after the dispute window |
| cancelled | Illegal — terminal | Illegal — terminal | Illegal — terminal | — | Illegal — terminal |
Two cells in that 25-cell table are where most real defects live. Cancelling from paid is legal but must route through the refund path, so an implementation that simply writes cancelled leaves money stranded. And the illegal cells in the cancelled row are the ones quietly violated by an admin tool built in a hurry — which is how a terminal order comes back to life with no record of who revived it.
Two lifecycles pretending to be one column
The clearest symptom of a merged model is an enum that grows by multiplication. Add a seller-side step to a buyer-side machine and you do not get 1 more state, you get every combination: awaiting_acceptance, paid_awaiting_acceptance, accepted_awaiting_payment. Six buyer states and 4 seller states become 24 rows in a transition table nobody will maintain.
- The buyer lifecycle answers what the buyer is owed and what they have paid. It is driven by buyer actions and by money.
- The seller lifecycle answers what the seller has committed to and delivered. It is driven by seller actions and by time.
- They are coupled at a small number of points — usually 2 — and modelling those points explicitly is far cheaper than modelling their product.
- Where a market later adds a structurally different transaction, such as quote-then-accept alongside instant purchase, keeping the lifecycles separate is what makes it an additive change rather than a fork.
If your status names contain the word "and", you are not modelling one machine with more states. You are modelling two machines in one column, and paying for it in every query.
What else belongs in the order record
The state is one column on a record that has to stand on its own years later, in a dispute, an audit or a support conversation. The rest of the record is what makes that possible.
- The agreed terms, copied rather than referenced. An order that reads its price from a live listing changes when the seller edits it, which is why the listing snapshot a buyer actually bought is captured at creation.
- A reference to the unit of supply, in the shape that unit actually has — an interval, a seat, or a quantity. Getting that shape right is upstream of everything here and is decided in what your marketplace actually sells.
- Both parties, referenced by identity rather than embedded. Whether a seller is a row or a tenant changes how that reference behaves under isolation, which is argued in is a seller a tenant or a row.
- An idempotency key from the client action that created it, so a double-tapped checkout produces 1 order rather than 2 competing for the same unit.
- Flags, as named columns with their own meaning, rather than as states. A flag that gates an action should say which action, in the column name.
None of this is exotic, and all of it is expensive to add once real orders exist. Getting the order record right early is ordinary product engineering of the kind we scope in internal tools and operations software; the surrounding schema decisions sit under marketplace architecture and the transaction data model, part of our marketplace and two-sided platform work.
Frequently asked questions
Short answers to the follow-ups this page tends to raise.
What is an order state machine in a marketplace?
It is the set of states an order can occupy plus the transitions allowed between them, enforced so that no code path can write an illegal move. A value earns a place in it only if it changes which transitions are legal — payment does, because it unlocks fulfilment and closes off free cancellation. Values that merely record something true about the order, such as whether it was reviewed or flagged, belong in columns beside the state rather than inside it.
What is the difference between an order state and an order flag?
A state changes what may happen next; a flag changes who may act or what is displayed. Disputed is the clearest example: an order under dispute can usually still be fulfilled, so the value constrains actors and payouts rather than transitions, which makes it a flag on the order and a state on the dispute record. The practical test is whether removing the value from the enum would make any transition illegal that is currently legal.
Should refunded be an order state?
No. Refunds belong to the payment lifecycle, because an order can be partially refunded, refunded in several instalments, or refunded after it completed — none of which has a sensible single position in an order enum. Model the refund on the payment record and let the order state describe what happened to the transaction itself: cancelled, returned, or completed.
How many order states should a marketplace have?
Most transacting marketplaces settle at 5 or 6 once flags and payment states have been removed, and an enum much beyond that usually signals 2 lifecycles merged into 1 column. The number matters less than the transition table: if you cannot write out every legal move on one page and mark the illegal cells, the machine is too large to enforce and too large to test.
- order model
- state machine
- data model
- transactions
The work behind this page
Builds from our portfolio that this page draws on.
Low Latency Food Ordering Platform
Unified events operations platform: vendor management, order tracking, payments, automated settlements.
MarketplaceDockQueue
A dock scheduling and yard management platform that lets carriers self-book dock appointments, assigns the right door on arrival, and tracks every trailer's dwell and detention live.
LogisticsRead next
- Listing snapshot: the version of the offer the buyer agreed toAn order that reads its terms from a live listing is an order whose terms change when the seller edits. The snapshot is the copy that makes it defensible.definition
- The unit of supply: the thing your marketplace is actually allocatingNot what you list, and not what you invoice: the unit of supply is the smallest thing two buyers can want at once when only one of them can have it.definition
- Orders that sit in pending forever: tracing the transition that never firedPending is not one problem. It is orders waiting on an event that never arrived, orders waiting on a human nobody notified, and orders in a state with no way out — and one query separates them.diagnostic
- Availability: a set of intervals, not a grid of day cellsAvailability is not a stored fact. It is the answer to a question, computed from recurring rules, exceptions and what has already been consumed — and a day-cell table is a cache of that answer.definition
- Available in search, gone at checkout: locating the stale layerRe-ask the source of truth at the moment of failure. If it agrees with the index, your two paths apply different rules; if it disagrees, something is holding a stale copy.diagnostic
- Inventory hold: the row that exists so the second buyer is refusedA hold is a record, not a screen state. It names one unit of supply, one claimant, one expiry and one reason — and while it lives, nothing else may overlap that unit.definition
Working on something in this space?
Tell us where you are in a sentence or two. We'll tell you honestly whether we're the right team, and what a sensible first slice of the work looks like.
Start the conversation