Key takeaways
- Store business state; derive visual labels from it.
- Every transition needs a source, trigger, destination and permitted actor.
- Rejected transitions deserve tests as much as happy paths.
- History and current state answer different questions.
Interface-first planning starts with a page, then adds controls until the page feels capable. State-first planning starts with truths: a request is awaiting a decision, accepted, declined, withdrawn or cancelled. Only then does it ask which controls are honest in each condition.
Consider an invented booking request for a shared room. A requester proposes a date. A coordinator accepts or declines. The requester may withdraw while a decision is pending. After acceptance, either party may cancel, but the record remains visible. No actual booking service is implied here.
Write the nouns, then the arrows
Start with the smallest set of mutually exclusive states. “New” is weak because it says nothing about what can happen. “Awaiting decision” names the obligation. “Accepted” and “declined” name outcomes. “Withdrawn” means the requester stopped the request before a decision. “Cancelled” means an accepted booking will no longer happen.
Original state chart
The chart intentionally has no arrow from declined to accepted. Reconsideration could be a valid later requirement, but adding it changes audit expectations and user messaging. A new request is simpler for this release. Likewise, withdrawn does not mean deleted. Deletion would erase the fact that a request once existed.
Add actors and guards
An arrow without an actor is incomplete. The coordinator can accept or decline only an awaiting request. The requester can withdraw only their own awaiting request. Cancellation from accepted may be available to both roles, though each trigger might record a different reason.
| From | Trigger | Actor | To | Guard |
|---|---|---|---|---|
| Awaiting | Accept | Coordinator | Accepted | Date remains available |
| Awaiting | Decline | Coordinator | Declined | Reason selected |
| Awaiting | Withdraw | Requester | Withdrawn | Own request |
| Accepted | Cancel | Either role | Cancelled | Reason recorded |
A guard checks facts beyond the source state. If availability changes between proposal and decision, acceptance must fail cleanly rather than overwrite reality. The visible interface may disable “accept”, but the behaviour rule still needs to check.
Design rejections beside transitions
For every row, write at least one rejected move. A requester tries to accept their own request. A coordinator tries to decline an already accepted request. Two coordinator decisions arrive close together. A stale page attempts withdrawal after acceptance. These are not exotic edge cases; they are direct consequences of the model.
The user-facing response should describe the current condition and next action: “This request was accepted before your withdrawal was recorded. Refresh the request to see its current state.” It should not expose datastore keys or internal call details. The failure classification matrix helps distinguish this healthy rejection from an unavailable dependency.
Keep current state separate from history
A status field answers “what is true now?” An event history answers “how did it become true?” Small projects often need only current state at first. Yet cancellation reasons and accountability may require history. Decide explicitly. Do not smuggle history into an ever-growing note field.
A modest event record might contain request identifier, prior state, new state, actor identifier, reason code and recorded time. That carries privacy and retention implications. If the release has no need to explain prior transitions, current state plus a modified time may be enough.
Let the interface follow
Once the table holds, interface decisions become less theatrical. An awaiting card shows accept and decline to a coordinator, withdraw to its requester, and no cancellation control. Accepted shows cancel. Terminal states show no action, perhaps only their reason. Labels are derived; permissions are enforced below the interface.
Pair this exercise with the data-shape annotation: every guard must have available data. Then map rejected arrows into the risk-led verification plan. That sequence is less glamorous than sketching screens. It is also harder to fake.
Honest limitation
A simple finite-state chart becomes unwieldy when activities run concurrently, have time windows, or require compensating actions. Real-time, regulated, high-scale, medical, financial, safety-critical and security-critical systems need specialist modelling and independent assurance beyond this editorial example.