Status Transitions
Most systems store a status as a value in a dropdown and hope nothing sets it to something silly. Documents here define the lifecycle as a set of permitted transitions instead — from this status, to that status — and anything not declared cannot happen.
Why transitions rather than statuses
The difference matters the first time somebody tries to reopen a posted invoice. With a list of statuses, nothing prevents it; the field simply accepts a new value, and a document that has already hit the ledger quietly returns to draft. With transitions, "a posted invoice cannot return to draft" is an enforced property of the system rather than a rule people are asked to remember.
It also documents itself. Reading the transition list tells you exactly what can happen to a document and in what order, which is the question a new administrator, an auditor or your future self will ask.
Defining a lifecycle
Add one row per permitted move, naming the document type, the status it moves from and the status it moves to. A typical sales invoice might allow draft to approved, approved to posted, approved back to draft, and posted to cancelled — and nothing else. Note what is absent: there is no route from posted back to draft, and that absence is the whole point.
Terminal statuses
A status with no outbound transitions is terminal. Cancelled and posted are usually terminal or nearly so. Be deliberate about this — a terminal status is a promise that the document will not change again, which is what makes it safe for the ledger and for reporting to rely on.
Who may make a move
Transitions are about what is structurally possible, not about who is allowed. Combine them with security groups and field-level permissions so that approving a document is both a legal move and a move only certain people can make.
Designing the lifecycle
Start with the smallest set that reflects reality, and resist adding a status for every internal nuance. Every status multiplies the transitions you must think about, and a lifecycle nobody can hold in their head will be worked around. If a status exists only so somebody can filter a list, a flag or a date column is usually the better answer.