The order lifecycle's states, why some transitions must be forbidden outright, the difference between customer-initiated and warehouse-initiated updates, and defining SLA requirements for how fast a state change reaches the customer.
Published September 23, 2026
CREATED → PAID → SHIPPED → DELIVERED
↘ CANCELLED
↘ REFUNDED
A typical order lifecycle moves through: created (the order exists, payment not yet confirmed), paid (payment succeeded — see Payment — Core Flow's authorize/capture state machine, which this state directly depends on), shipped (handed to a carrier), delivered (confirmed received). Cancelled and refunded are exit paths that can branch off at different points, not a single linear "failure" state — cancelling a CREATED order is trivial (nothing has shipped or been charged); cancelling a SHIPPED order means something entirely different (an in-flight return, not a simple cancellation).
An order moving from DELIVERED back to CREATED is nonsensical — the state machine needs to explicitly ENUMERATE which transitions are valid and reject everything else, rather than allowing any state field to be set to any value by whichever code path happens to touch it. This is the same discipline as Payment — Core Flow's guarded state transitions ("you cannot capture a payment that was never authorized") — an order's state machine is exactly as consequential to get right, since an invalid transition here means real inventory, shipping, and customer-communication inconsistencies, not just a data-quality issue.
Customer-initiated: cancel (only while CREATED or PAID, before shipment)
Warehouse-initiated: mark as shipped, mark as delivered (via carrier webhook)
Support-initiated: process a return/refund after delivery
Unlike a single-actor state machine (Payment — Core Flow's flow is driven almost entirely by the payment processor's responses), an order's state is updated by MULTIPLE independent actors — the customer (cancellation), the warehouse/fulfillment system (shipped), the carrier (delivered, via a tracking webhook), and support staff (refunds, returns). Each actor should only be ALLOWED to trigger specific transitions from specific states — a customer shouldn't be able to mark their own order as "delivered," and the warehouse system shouldn't be able to trigger a refund. This access-scoping is a genuine requirement, not an implementation detail to figure out later.
Different transitions have different acceptable latency for reaching the customer-visible order status: a payment confirmation probably needs to reflect within seconds (the customer is actively waiting on the checkout page); a "shipped" status update from the warehouse might have an acceptable few-minutes-to-an-hour lag; a carrier's "delivered" webhook processing within a few minutes is usually fine. Stating this explicitly PER TRANSITION (not one blanket "real-time" requirement for everything) is what actually justifies later architecture decisions — Event-Driven Architecture Patterns' choice between event notification and event-carried state transfer, for instance, is partly informed by how urgently a given state change needs to propagate.
Q: Why can't 'cancelled' just be one of the linear states in the main flow? A: Because the CONSEQUENCES of cancelling differ completely depending on when it happens — cancelling before payment is a no-op cleanup; cancelling after shipment requires an actual physical return process — modeling these as one flat 'CANCELLED' state loses this distinction, which is exactly why OMS — State Machine Design treats cancellation-after-shipment as its own explicit transition into a return flow, not a simple revert.
Q: How would you handle an order that needs to be cancelled by support on the customer's behalf? A: This needs its own explicit actor/permission scoping decision — is 'support-initiated cancellation' the same transition as 'customer-initiated cancellation' with different allowed actors, or a genuinely distinct transition with different downstream consequences (e.g., different refund-reason-code tracking)? Stating this explicitly in requirements avoids the ambiguity showing up as an undocumented special case in the implementation later.
Q: Is there a risk in having overly strict transition SLAs? A: Yes — an unrealistically tight SLA (e.g. demanding sub-second propagation for every transition regardless of actual customer need) can force unnecessarily expensive architecture (synchronous calls everywhere instead of appropriate async messaging) for no real customer-facing benefit; SLA requirements should be driven by genuine user expectation per transition type, not a uniform maximum-urgency default.
Q: How does this connect to the read-model considerations in a system like this? A: An order's current status is read FAR more often than it changes (a customer repeatedly checking their order status page) — this read-heavy skew is exactly the kind of signal Back-of-Envelope Estimation's read:write ratio reasoning says should drive caching decisions for the order-status read path, independent of how the underlying state machine itself is implemented.