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.
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