Order State Machine
An order progresses through a defined set of states with controlled transitions:
CART → PLACED → PAYMENT_PENDING → CONFIRMED → PICKING → PACKED → SHIPPED → DELIVERED
↘ PAYMENT_FAILED → CANCELLED
↘ CANCELLED (user cancellation before payment)
CONFIRMED → CANCELLED (cancellation window, before picking starts)
DELIVERED → RETURN_REQUESTED → RETURNING → RETURNED
State transitions are validated in the domain layer — an order in SHIPPED state cannot transition directly to CANCELLED. Each transition is persisted as an immutable event in an order_events table, giving a full audit trail. The current state is denormalized into the orders table for query performance.
Order Schema
orders:
order_id UUID PRIMARY KEY DEFAULT gen_random_uuid()
user_id UUID NOT NULL
status VARCHAR(30) NOT NULL DEFAULT 'PLACED'
total_amount NUMERIC(12,2) NOT NULL
currency CHAR(3) NOT NULL DEFAULT 'USD'
shipping_addr JSONB NOT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
version INT NOT NULL DEFAULT 1 -- optimistic locking
order_items:
item_id UUID PRIMARY KEY DEFAULT gen_random_uuid()
order_id UUID NOT NULL REFERENCES orders(order_id)
product_id UUID NOT NULL
sku VARCHAR(100) NOT NULL
quantity INT NOT NULL CHECK (quantity > 0)
unit_price NUMERIC(10,2) NOT NULL
subtotal NUMERIC(12,2) GENERATED ALWAYS AS (quantity * unit_price) STORED
Inventory Reservation
Inventory reservation uses a two-phase approach to avoid overselling while keeping reservation windows short:
- Soft reserve (on order placement): Decrement
available_quantityby order quantity. The item is held for this order but not yet committed. - Hard deduct (on payment confirmation): Move quantity from reserved to committed. Update
reserved_quantityandsold_quantity. - Release (on cancellation or timeout): Increment
available_quantityback. A background job releases soft reserves older than the reservation timeout (e.g., 30 minutes for unpaid orders).
inventory:
product_id UUID PRIMARY KEY
sku VARCHAR(100) NOT NULL UNIQUE
total_quantity INT NOT NULL
reserved_quantity INT NOT NULL DEFAULT 0 -- soft reserved
sold_quantity INT NOT NULL DEFAULT 0 -- hard deducted
available_quantity INT GENERATED ALWAYS AS
(total_quantity - reserved_quantity - sold_quantity) STORED
version INT NOT NULL DEFAULT 1 -- optimistic locking
Optimistic Locking for Inventory
Concurrent orders for the same SKU race on inventory. Optimistic locking detects conflicts without row-level locks:
-- Attempt soft reserve with version check
UPDATE inventory
SET reserved_quantity = reserved_quantity + $qty,
version = version + 1
WHERE product_id = $product_id
AND version = $expected_version
AND available_quantity >= $qty;
-- If 0 rows updated: either version mismatch (retry) or insufficient stock (reject)
A version mismatch means another transaction modified the row concurrently. The service reloads the current version and retries up to N times. After N retries, return a conflict error to the caller. For high-contention SKUs (flash sales), use a Redis counter for the hot path and reconcile with the database asynchronously.
Event-Driven Coordination
Services coordinate through events rather than direct calls, keeping each service independent:
Order Service publishes: OrderPlaced, OrderConfirmed, OrderCancelled
Payment Service listens: OrderPlaced → initiate payment
Payment Service publishes: PaymentCaptured, PaymentFailed
Inventory Service listens: OrderPlaced → soft reserve
PaymentCaptured → hard deduct
OrderCancelled → release reserve
Fulfillment Service listens: PaymentCaptured → create shipment task
Events are published to Kafka topics partitioned by order_id, ensuring ordered delivery per order. Each service maintains its own state and handles idempotent event processing (deduplicate on event_id).
Saga Compensation
If payment fails after inventory has been soft-reserved, the saga must compensate:
OrderPlaced event → inventory soft-reserves → payment initiated
PaymentFailed event → OrderService transitions to PAYMENT_FAILED
→ publishes OrderCancelled
OrderCancelled event → inventory releases soft reserve
Each compensating step is idempotent. If the OrderCancelled event is delivered twice, the inventory release must be safe to apply twice (check current status before releasing). Saga state can be tracked in a sagas table (saga_id, order_id, current_step, status) for observability and manual intervention.
Split Shipments and Returns
A single order may ship from multiple warehouses if items are not co-located. The fulfillment service creates one shipment record per warehouse assignment:
shipments:
shipment_id UUID PRIMARY KEY
order_id UUID NOT NULL REFERENCES orders(order_id)
warehouse_id UUID NOT NULL
status VARCHAR(30) NOT NULL -- CREATED, PICKED, PACKED, SHIPPED, DELIVERED
tracking_num VARCHAR(100)
carrier VARCHAR(50)
shipped_at TIMESTAMPTZ
delivered_at TIMESTAMPTZ
shipment_items:
shipment_id UUID REFERENCES shipments(shipment_id)
item_id UUID REFERENCES order_items(item_id)
quantity INT NOT NULL
The order transitions to DELIVERED only when all shipments are in DELIVERED state. Partial delivery is surfaced to the customer through shipment-level tracking rather than order-level status. Returns create a return_request linked to specific shipment items, triggering the reverse logistics flow and refund pipeline.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What are the states in an order management state machine?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A typical order state machine transitions through states such as PENDING, CONFIRMED, PAYMENT_AUTHORIZED, FULFILLMENT_IN_PROGRESS, SHIPPED, DELIVERED, and CANCELLED or REFUNDED. Each transition is triggered by a domain event and enforced by guard conditions, ensuring invalid state jumps (e.g., shipping a cancelled order) are rejected at the application layer.”
}
},
{
“@type”: “Question”,
“name”: “How is inventory reservation handled to prevent oversell?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Inventory reservation uses an optimistic-locking or compare-and-swap update on a reserved_quantity column, decrementing available stock atomically when an order is placed. A reservation TTL (e.g., 15 minutes) is enforced by a background job that releases expired holds, returning stock to the available pool if payment is not completed in time.”
}
},
{
“@type”: “Question”,
“name”: “How does a saga pattern handle payment failure after inventory reservation?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “In a choreography-based saga, a PaymentFailed event triggers a compensating InventoryReleaseCommand that increments available_quantity back to its pre-reservation value. In an orchestration-based saga, the central saga orchestrator explicitly invokes the compensating transaction on the inventory service, then marks the order as CANCELLED and publishes a notification event.”
}
},
{
“@type”: “Question”,
“name”: “How are split shipments modeled?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A parent order is decomposed into one or more Fulfillment records, each linked to a warehouse location and carrying its own line items, carrier tracking number, and independent state machine. The parent order's status is derived from the aggregate state of its fulfillments, becoming FULLY_SHIPPED only when every fulfillment reaches the SHIPPED state.”
}
}
]
}
See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering
See also: Shopify Interview Guide
See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems