Turn web-app statuses into an explicit state model
A status column is not a state model. Before building a web app, name the business records, how they relate, which facts must remain true and which events may move a record from one state to another. The two copyable matrices below turn an informal process into rules a business owner can review and a delivery team can test.
Start with one record that people need to follow
Choose a concrete journey, such as an order moving from submission to fulfilment. Ask what the team needs to identify, correct, search, link and retain. A screen named “Orders” is not yet a record definition: one customer might have several orders, and one order might contain several items or versions of an attached design. Write the nouns in the team’s own language and give each persistent record a stable identifier.
Separate a record from a field on that record. A delivery address may be a set of order fields if nobody tracks it independently. An artwork version needs its own identity when a team must compare versions, approve one specific version and keep that decision after another version arrives. Microsoft’s domain-model guidance distinguishes entities by continuing identity and says the model should capture business language, rules and constraints. It also cautions that a simple data-editing app may not need elaborate domain-driven design patterns. Use the distinction to clarify the work, not to mandate an architecture.
Copy the record and relationship matrix
Create one row per important record type. A shared document is enough at this stage; do not put real customer or staff data into a public worksheet. Use the same names in the process sketch, app screens and later acceptance criteria. Resolve a disputed relationship with the process owner before treating a database diagram as settled.
- Record | What one record represents | Stable identifier | Required facts | Related record and number allowed | Owner or source | Change or deletion rule | Open question.
- Record: choose a singular business noun. Define the boundary so two people would identify the same real-world thing as one record.
- Identifier and required facts: state what uniquely distinguishes records, which values must exist at creation and which may arrive later.
- Relationship: say “one customer can have many orders; each order belongs to one customer”, including whether the relationship may ever be absent. Record any many-to-many link explicitly.
- Owner and changes: name the team or system that may correct a fact, what happens to linked records when one is archived or deleted, and which history must be preserved.
- Open question: keep uncertain cardinality, ownership and retention choices visible. A guessed rule becomes expensive when existing records violate it.
Give each status one meaning
A single “status” field often tries to answer several questions. An order can be in production while its invoice is unpaid and a notification is queued. Those are separate facts with different owners and events. Write the main lifecycle in business words, then identify independent statuses, dates or derived labels. Avoid multiplying every combination into one giant list such as “in-production-unpaid-email-pending”.
For the main lifecycle, define where a record starts, where it may finish, and what an exceptional or reversible path means. A state is a condition the system can verify, not merely a color on a dashboard. W3C’s SCXML specification formalizes states and transitions triggered by events with optional guard conditions. This planning worksheet borrows those useful concepts; the app does not need to implement SCXML.
Copy the transition and invariant matrix
Write one row for each permitted move, including cancellation, correction and reopening where the business allows them. A row should describe an event and its preconditions rather than “user changes dropdown to Done”. Record a negative test beside it: the attempted move must leave the stored record unchanged when its conditions are false. The permission matrix handles who may act on which record; this matrix handles whether the action is valid for the record’s current business state.
- Record | From state | Event or command | Guard: facts required now | To state | Facts changed together | Rejection and recovery | Business owner and test ID.
- From and to: use exact state names and include a start state. Define whether a terminal state can ever be reopened and by which explicit command.
- Event and guard: say what happened and which record version, related fact or deadline must be checked when it happens. “Artwork approved” is more precise than “advance”.
- Facts changed together: include the state, the decision’s record/version reference and any required timestamp or relationship that must stay consistent.
- Rejection and recovery: say what the user sees if the move is invalid, stale or attempted twice, and who resolves an uncertain external result.
- Invariant: below the rows, write rules that must always hold, such as “a production order references its current approved artwork version” and “a fulfilled order has a recorded completion event”. Give each one a counterexample test.
Hypothetical example: a print-studio order
Imagine a small print studio building an internal order app. This is an invented planning example, not Kinetivy client work or a deployed system. The team has a customer, an order and several artwork versions. Each order belongs to one customer; each artwork version belongs to one order and carries its own version identifier. The order lifecycle is draft → submitted → artwork review → ready for production → in production → fulfilled. “Cancelled” is a separate end state available only under agreed conditions. Payment and message-delivery statuses remain separate from the production lifecycle.
In the record matrix, an Order row could read: “Order | One requested print job | Order ID | Customer ID, requested product, quantity | One customer; many artwork versions | Studio operations | Preserve fulfilled orders; correction creates a recorded change | Can an order contain two products?” The question remains open until the team decides whether those are two lines on one order or two orders. The model makes that ambiguity visible before data is imported.
In the transition matrix, one row could read: “Order | Artwork review | Approve artwork version | Referenced version belongs to this order and is the current submitted version | Ready for production | Save order state and approved version ID together | Reject a stale approval and ask the reviewer to open the latest version | Production owner; T1.” A second row covers ready for production → in production only when a production slot is assigned and the approved artwork version is still current. If a new version arrives before production, the order returns to artwork review and that new version needs its own approval. A third row covers in production → fulfilled only when completion is recorded. Submission cannot jump straight to fulfilment, and approving version 2 must not silently approve version 3.
Useful invariant tests follow from those rows: try approving an artwork version from another order, uploading version 3 after version 2 was approved and then starting production, starting with no assigned slot, fulfilling twice, and cancelling after fulfilment. The version-3 attempt must leave the order out of production until that version is approved. A wrong-order or stale version must not change the approved version or state; a missing slot must leave the order ready for production; a repeated fulfilment or post-fulfilment cancellation must leave the order fulfilled without a second completion event. Each test has an expected stored result, not just a button state. If the studio’s real process allows rework, add an explicit rework event and state; do not quietly overwrite “fulfilled” to “artwork review” and erase what happened.
Check the rules where records change
A browser can explain available actions, but a direct request, retry or background job can reach the same record. OWASP’s business-logic guidance says workflow transitions should be checked against the current state and valid combinations enforced on the server. Validate the record version and related facts at the time of change, then apply the checked change atomically through the chosen storage mechanism. If two people act on the same old state, decide which action wins and make the other see a clear conflict; do not assume a disabled button prevents a race.
Ask the delivery team how the chosen storage layer will enforce stable identity, required values and links. PostgreSQL’s documentation describes primary keys, not-null, unique, check and foreign-key constraints, including referential integrity; those are examples of implementation controls, not a requirement to use PostgreSQL. A field-value constraint alone cannot prove an entire business transition occurred correctly. The app must apply the transition rule and keep related changes consistent in its chosen architecture.
Have the process owner walk through a normal record, a correction, cancellation, stale action and disputed relationship. Sign off the matrix before interface labels and imports are frozen. Revisit it when the business adds a new product, role, external system or meaning of “done”. The data-migration guide covers moving existing records into an approved model; the acceptance-testing guide covers proving the agreed rules in a release.
Questions worth asking.
Do we need a state machine for every field?
No. Model a lifecycle when the next valid action depends on the current condition and there are meaningful exceptions. Keep ordinary descriptive fields as fields, and give independent processes such as payment or notification delivery their own status only when the team actually needs to track them.
Can we begin with a status dropdown?
It can be a useful interface after the allowed transitions are defined. Write which event each choice represents, what facts must be true, and what should happen when the record changes elsewhere before a user saves.
Should an old state be overwritten when a mistake is corrected?
Define a correction or reopening action and preserve enough history to explain the current record. The exact history and retention requirements depend on the business and applicable obligations; the transition worksheet should identify the owner who decides them.
How detailed should the first model be?
Cover the first release’s real records, core journey and exceptions that would otherwise mislead a user or corrupt a record. Keep uncertain future branches as open questions. Add complexity only when an actual decision or test requires it.
Sources & further reading
- Microsoft Learn: Design a microservice domain model ↗︎
- W3C: State Chart XML (SCXML) ↗︎
- OWASP: Business Logic Security Cheat Sheet ↗︎
- PostgreSQL: Constraints ↗︎
Examples in this guide are illustrative. Read about our editorial approach.