Order Management System Design
Design a scalable enterprise Order Management System with end-to-end flow — from cart checkout through payment, fulfillment, shipping, and delivery. Covers requirements, capacity estimation, order state machine, Saga pattern for distributed transactions, inventory reservation, warehouse allocation, returns, and operational readiness.
1-Hour Interview Roadmap
| Time | Topic |
|---|---|
| 0 – 5 min | Requirements clarification |
| 5 – 10 min | Capacity estimation |
| 10 – 18 min | High-level architecture + core services |
| 18 – 28 min | Order creation flow — checkout → payment → inventory reservation |
| 28 – 38 min | Order state machine + Saga pattern for distributed consistency |
| 38 – 46 min | Fulfillment pipeline — warehouse allocation, picking, packing |
| 46 – 52 min | Database design + API design |
| 52 – 57 min | Returns, cancellations, and failure handling |
| 57 – 60 min | Trade-offs + common interview mistakes |
What Are We Building?
An enterprise Order Management System (OMS) that manages the complete lifecycle of a customer order — from the moment a customer places an order through payment, warehouse fulfillment, shipping, delivery, and post-delivery actions like returns and refunds.
The OMS is the central nervous system of an e-commerce or retail operation. Every other system — inventory, payments, logistics, notifications, customer service — integrates with it. Every order state change cascades into actions across multiple services.
Scale reference: Amazon processes ~1.6 million orders per day. Shopify processes ~5 million orders per day across all merchants. A mid-tier enterprise retailer processes 50,000–500,000 orders per day. Design for 500,000 orders per day (~6 orders/second average, ~60 orders/second peak).
Key unique challenges:
- Distributed consistency — order creation involves payment, inventory, and order record writes across services; all three must succeed or all three must roll back
- State machine correctness — an order has a strict lifecycle; invalid transitions (e.g., shipping a cancelled order) must be prevented at the database level
- Inventory races — multiple customers checking out the same last item simultaneously must not result in overselling
- Idempotency — payment retries, webhook redeliveries, and network retries must not create duplicate orders or double-charges
- End-to-end auditability — every state transition must be permanently logged for customer service, compliance, and analytics
Step 1 — Requirements
Functional Requirements
| # | Requirement |
|---|---|
| 1 | Customer can place an order from a cart with one or more line items |
| 2 | System verifies item availability and reserves inventory at order creation |
| 3 | System processes payment and handles authorization, capture, and failure |
| 4 | System creates a confirmed order record upon successful payment |
| 5 | System routes confirmed orders to the appropriate fulfillment warehouse |
| 6 | Warehouse operators can manage order picking, packing, and dispatch |
| 7 | System generates shipping labels and dispatches orders to carrier (FedEx, UPS, DHL) |
| 8 | Customer can track order status in real time from confirmation to delivery |
| 9 | Customer can cancel an order before it enters the picking phase |
| 10 | Customer can initiate a return after delivery; system manages refund |
| 11 | System sends notifications at every major status transition |
| 12 | Operations team can view order dashboards, SLA breaches, and fulfillment metrics |
Non-Functional Requirements
| # | Requirement |
|---|---|
| 1 | Order placement API response within 2 seconds (p99) |
| 2 | Zero overselling — inventory race conditions must be handled with strong consistency |
| 3 | Zero order loss — no order can be silently dropped; all failures are retried or surfaced |
| 4 | Exactly-once payment — network retries must never result in double charges |
| 5 | Order status must be eventually consistent within 5 seconds of a state change |
| 6 | High availability — 99.99% uptime for order placement API |
| 7 | Full audit trail — every state transition is permanently logged with actor and timestamp |
| 8 | Horizontally scalable — throughput grows by adding workers, not re-architecting |
Out of Scope
- Product catalog and search
- Shopping cart management (assume cart service provides cart contents)
- Payment processor internals (assume abstraction over Stripe / Adyen)
- Carrier logistics and last-mile routing
- Fraud detection (assume a separate fraud service decision is provided)
- Seller/vendor portals
Step 2 — Capacity Estimation
Traffic Estimates
Daily orders: 500,000
Average OPS (orders/sec): 500K / 86,400 = ~6 orders/sec
Peak OPS (10× surge): ~60 orders/sec
Line items per order (avg): 3
Line item writes per day: 500K × 3 = 1.5M writes/day
Order status reads (tracking):
Each order queried ~20 times (customer + ops + carrier callbacks)
500K × 20 = 10M reads/day = ~116 QPS average, ~1,160 QPS peak
Fulfillment events per order: ~8–12 state transitions
Event writes/day: 500K × 10 = 5M events/day
Storage Estimates
Per order record:
Order header: ~800 bytes
Line items (avg 3): ~300 bytes each = 900 bytes
Audit log events (avg 10): ~200 bytes each = 2,000 bytes
Total per order: ~3.7 KB
Annual storage:
500K orders/day × 3.7 KB × 365 days = ~675 GB/year
Inventory reservation records:
500K reservations/day × 200 bytes × 2 days TTL = ~200 MB active
Kafka event retention (7 days):
5M events/day × 500 bytes × 7 days = ~17.5 GB
Key Insights
- Write-moderate, read-heavy — tracking queries dominate at 20 reads per order
- Inventory is the hottest write path — reservation + release on every order; must be strongly consistent
- Storage is very manageable — 675 GB/year; hot data fits in a well-indexed PostgreSQL cluster
- Peak factor matters — holiday sales create 10× spikes; queue-based architecture absorbs bursts without data loss
- Event volume is large — 5M events/day requires a dedicated events table with partitioning strategy
Step 3 — High-Level Architecture
flowchart TD
Client["Customer App\n/ Web"]
AG["API Gateway\nAuth + Rate Limiting"]
CS["Cart Service"]
OS["Order Service\n(Orchestrator)"]
PS["Payment Service"]
IS["Inventory Service"]
FS["Fulfillment Service"]
SS["Shipping Service"]
NS["Notification Service"]
RS["Returns Service"]
ODB[("PostgreSQL\nOrder DB")]
IDB[("PostgreSQL\nInventory DB")]
KF["Kafka\nEvent Bus"]
REDIS["Redis\nIdempotency + Cache"]
Client --> AG
AG --> CS
AG --> OS
AG --> RS
OS --> PS
OS --> IS
OS --> ODB
OS --> REDIS
OS --> KF
KF --> FS
KF --> SS
KF --> NS
FS --> IDB
SS --> ODB
RS --> PS
RS --> IS
RS --> ODB
Component Responsibilities
| Component | Responsibility |
|---|---|
| API Gateway | Authenticate customer JWT, rate-limit per customer, route to services |
| Cart Service | Maintain cart state; provide cart contents at checkout time |
| Order Service | Central orchestrator — creates orders, coordinates payment + inventory, manages state machine |
| Payment Service | Authorize and capture payments; handle refunds; idempotent via payment_intent_id |
| Inventory Service | Manage stock levels; reserve stock at order creation; release on cancel/return |
| Fulfillment Service | Receive confirmed orders; allocate to warehouse; manage pick/pack/dispatch workflow |
| Shipping Service | Generate labels, integrate with carriers, track shipment events |
| Notification Service | Send order confirmation, dispatch, and delivery notifications (email, SMS, push) |
| Returns Service | Manage return requests, RMA generation, refund orchestration |
| Order DB | PostgreSQL — orders, line items, audit events (source of truth) |
| Inventory DB | PostgreSQL — stock levels, reservations (strongly consistent writes) |
| Kafka | Event bus — decouples Order Service from Fulfillment, Shipping, Notifications |
| Redis | Idempotency key store, order status cache for high-read tracking API |
Step 4 — End-to-End Order Flow
This is the complete journey from customer clicking "Place Order" to the order being confirmed in the system.
sequenceDiagram
participant C as Customer App
participant AG as API Gateway
participant OS as Order Service
participant REDIS as Redis
participant IS as Inventory Service
participant PS as Payment Service
participant ODB as Order DB
participant KF as Kafka
C->>AG: POST /v1/orders {cart_id, payment_method, address, idempotency_key}
AG->>OS: Forward authenticated request
OS->>REDIS: SET NX idempotency:{customer_id}:{idempotency_key} TTL=24h
REDIS-->>OS: OK (new) or EXISTS (duplicate → return cached response)
OS->>IS: ReserveInventory(line_items)
IS-->>OS: reservation_id (or InsufficientStock error)
OS->>PS: AuthorizePayment(payment_method, amount, idempotency_key)
PS-->>OS: payment_auth_id (or PaymentDeclined error)
OS->>ODB: BEGIN TRANSACTION
OS->>ODB: INSERT order {status: PAYMENT_AUTHORIZED}
OS->>ODB: INSERT order_line_items
OS->>ODB: INSERT order_audit_event {CREATED}
OS->>ODB: COMMIT
OS->>PS: CapturePayment(payment_auth_id)
PS-->>OS: payment_captured: true
OS->>ODB: UPDATE order SET status = CONFIRMED
OS->>ODB: INSERT order_audit_event {PAYMENT_CAPTURED}
OS->>KF: Publish order.confirmed {order_id, line_items, warehouse_hint}
OS-->>C: 201 {order_id, status: "confirmed", estimated_delivery}
Why This Exact Sequence?
The order of operations is critical. Every step is chosen to minimize failure blast radius:
| Step | Why This Order |
|---|---|
| Idempotency check first | Prevents duplicate orders from retries before any side effects occur |
| Reserve inventory before payment | Avoids charging customers for out-of-stock items |
| Authorize before capture | Authorization is reversible; capture is final — gives time to validate everything before committing funds |
| Write to DB before publishing to Kafka | DB commit is the source of truth; Kafka event is downstream. If Kafka fails, the order exists and can be re-published |
| Capture after DB write | Order is recorded before funds are taken; prevents charging customer with no order record |
Step 5 — Order State Machine
An order moves through a strictly defined sequence of states. No state transition is allowed unless explicitly permitted. Enforcing this at the database level (with a check constraint or application-level guard) prevents bugs where an order is shipped after cancellation or refunded without being delivered.
stateDiagram-v2
[*] --> PENDING : Customer submits order
PENDING --> PAYMENT_AUTHORIZED : Payment authorization succeeds
PENDING --> PAYMENT_FAILED : Authorization declined or timeout
PENDING --> CANCELLED : Customer cancels before payment
PAYMENT_AUTHORIZED --> CONFIRMED : Payment captured successfully
PAYMENT_AUTHORIZED --> CANCELLED : Capture fails — authorization voided
CONFIRMED --> ALLOCATED : Warehouse allocates stock to order
CONFIRMED --> CANCELLED : Customer cancels (stock released, payment refunded)
ALLOCATED --> PICKING : Warehouse picker starts pick task
ALLOCATED --> CANCELLED : Cancellation allowed; stock released, refund initiated
PICKING --> PACKED : All items picked and packed
PICKING --> EXCEPTION : Item not found (short pick) — ops intervention needed
PACKED --> DISPATCHED : Carrier collects package; label scanned
DISPATCHED --> IN_TRANSIT : Carrier accepts package
IN_TRANSIT --> OUT_FOR_DELIVERY : Last-mile carrier has package
OUT_FOR_DELIVERY --> DELIVERED : Delivery confirmed (scan or signature)
OUT_FOR_DELIVERY --> DELIVERY_FAILED : Delivery attempt failed
DELIVERY_FAILED --> OUT_FOR_DELIVERY : Re-attempted
DELIVERY_FAILED --> RETURNED_TO_SENDER : Max attempts exceeded
DELIVERED --> RETURN_REQUESTED : Customer initiates return
RETURN_REQUESTED --> RETURN_IN_TRANSIT : Customer ships return
RETURN_IN_TRANSIT --> RETURN_RECEIVED : Warehouse receives return
RETURN_RECEIVED --> REFUNDED : Refund processed
PAYMENT_FAILED --> [*]
CANCELLED --> [*]
REFUNDED --> [*]
RETURNED_TO_SENDER --> [*]
Valid Transitions Table
-- Enforce valid transitions at application layer
-- (or enforce via PostgreSQL trigger / check constraint)
Valid transitions:
PENDING → PAYMENT_AUTHORIZED, PAYMENT_FAILED, CANCELLED
PAYMENT_AUTHORIZED → CONFIRMED, CANCELLED
CONFIRMED → ALLOCATED, CANCELLED
ALLOCATED → PICKING, CANCELLED
PICKING → PACKED, EXCEPTION
PACKED → DISPATCHED
DISPATCHED → IN_TRANSIT
IN_TRANSIT → OUT_FOR_DELIVERY
OUT_FOR_DELIVERY → DELIVERED, DELIVERY_FAILED
DELIVERY_FAILED → OUT_FOR_DELIVERY, RETURNED_TO_SENDER
DELIVERED → RETURN_REQUESTED
RETURN_REQUESTED → RETURN_IN_TRANSIT
RETURN_IN_TRANSIT → RETURN_RECEIVED
RETURN_RECEIVED → REFUNDED
Any other transition → REJECTED with InvalidStateTransition error
Step 6 — Saga Pattern for Distributed Consistency
The order creation flow spans three services: Inventory (reservation), Payment (authorization + capture), and Order (record creation). There is no distributed transaction across these services. Instead, a Choreography-based Saga ensures consistency through compensating actions.
What is a Saga?
A Saga breaks a distributed transaction into a sequence of local transactions, each with a compensating transaction that undoes its effect if a later step fails.
flowchart LR
T1["T1: Reserve\nInventory\n(Inventory Service)"]
T2["T2: Authorize\nPayment\n(Payment Service)"]
T3["T3: Create\nOrder Record\n(Order DB)"]
T4["T4: Capture\nPayment\n(Payment Service)"]
C1["C1: Release\nReservation"]
C2["C2: Void\nAuthorization"]
T1 -->|"Success"| T2
T2 -->|"Success"| T3
T3 -->|"Success"| T4
T2 -->|"Fail"| C1
T3 -->|"Fail"| C2
C2 --> C1
T4 -->|"Fail"| C2
Compensation Scenarios
| Failure Point | Effect | Compensation |
|---|---|---|
| Inventory reservation fails | No stock available | Return error to customer; no compensation needed |
| Payment authorization fails | Card declined | Release inventory reservation |
| Order DB write fails | DB error | Void payment authorization; release inventory reservation |
| Payment capture fails | Rare — auth succeeded but capture failed | Release inventory reservation; order set to PAYMENT_FAILED |
Why Choreography vs Orchestration?
| Approach | Description | Used Here |
|---|---|---|
| Orchestration | Central saga orchestrator coordinates all steps, tracks state, and calls compensations | Used for complex sagas with many services and conditional paths |
| Choreography (chosen) | Each service reacts to events and publishes its own events; no central coordinator | Used here because order creation is a linear 4-step flow with clear failure boundaries |
The Order Service acts as the implicit orchestrator for the create flow — it calls each step sequentially and handles compensation inline. For more complex post-creation flows (fulfillment, shipping), Kafka events choreograph the pipeline.
Step 7 — Inventory Reservation
Inventory management is the most critical correctness requirement. Overselling — confirming orders for items that are out of stock — damages customer trust and creates fulfillment nightmares.
The Overselling Problem
sequenceDiagram
participant C1 as Customer 1
participant C2 as Customer 2
participant IS as Inventory Service
participant DB as Inventory DB
Note over DB: stock = 1 (last unit)
C1->>IS: Check stock for SKU-123
C2->>IS: Check stock for SKU-123
IS-->>C1: 1 unit available
IS-->>C2: 1 unit available
C1->>IS: Reserve SKU-123
C2->>IS: Reserve SKU-123
IS->>DB: UPDATE stock SET reserved = reserved + 1 WHERE sku = 'SKU-123' AND (available - reserved) > 0
Note over DB: Only one UPDATE succeeds due to atomic check
IS-->>C1: Reservation SUCCESS
IS-->>C2: Reservation FAILED — insufficient stock
Inventory Reservation Schema
CREATE TABLE inventory (
sku VARCHAR(50) NOT NULL PRIMARY KEY,
warehouse_id VARCHAR(50) NOT NULL,
total_quantity INT NOT NULL CHECK (total_quantity >= 0),
reserved_quantity INT NOT NULL DEFAULT 0 CHECK (reserved_quantity >= 0),
available_quantity INT GENERATED ALWAYS AS (total_quantity - reserved_quantity) STORED,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CHECK (reserved_quantity <= total_quantity)
);
CREATE TABLE inventory_reservations (
reservation_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
order_id UUID, -- NULL until order is confirmed
sku VARCHAR(50) NOT NULL REFERENCES inventory(sku),
quantity INT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE | RELEASED | COMMITTED
expires_at TIMESTAMPTZ NOT NULL, -- auto-expire stale reservations (e.g., 30 min)
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Atomic Reservation SQL
-- Reserve inventory atomically — fails if insufficient stock
WITH reservation AS (
UPDATE inventory
SET reserved_quantity = reserved_quantity + :quantity,
updated_at = NOW()
WHERE sku = :sku
AND (total_quantity - reserved_quantity) >= :quantity
RETURNING sku, available_quantity
)
INSERT INTO inventory_reservations (sku, quantity, expires_at)
SELECT :sku, :quantity, NOW() + INTERVAL '30 minutes'
FROM reservation
RETURNING reservation_id;
-- Returns 0 rows if stock is insufficient — no reservation created
Reservation Expiry
When a customer adds items to cart and begins checkout but never completes it, the reservation must be released automatically. Otherwise, inventory is permanently locked by abandoned checkouts.
Reservation TTL: 30 minutes from creation
Expiry job: Runs every 5 minutes
SELECT * FROM inventory_reservations
WHERE status = 'ACTIVE' AND expires_at < NOW()
→ Release: UPDATE inventory, set status = 'RELEASED'
On order confirmation:
→ Commit: UPDATE reservation status = 'COMMITTED', expires_at = NULL
→ Reservation now locked until shipment/cancellation
Step 8 — Fulfillment Pipeline
Once an order is confirmed, the Fulfillment Service takes ownership. It allocates the order to a specific warehouse, creates work tasks for pickers, and manages the physical flow of the order through the warehouse.
Fulfillment Flow
sequenceDiagram
participant KF as Kafka
participant FS as Fulfillment Service
participant WMS as Warehouse Mgmt System
participant Picker as Warehouse Picker
participant SS as Shipping Service
participant OS as Order Service
KF->>FS: order.confirmed event {order_id, line_items, customer_address}
FS->>FS: Select optimal warehouse\n(proximity, stock availability)
FS->>WMS: CreatePickTask {order_id, line_items, bin_locations}
FS->>OS: UpdateOrderStatus(order_id, ALLOCATED)
Picker->>WMS: Start pick task (scan order barcode)
WMS->>OS: UpdateOrderStatus(order_id, PICKING)
loop For each line item
Picker->>WMS: Scan item barcode + quantity confirm
end
Picker->>WMS: Mark pick complete → move to packing station
WMS->>OS: UpdateOrderStatus(order_id, PACKED)
FS->>SS: GenerateShippingLabel {order_id, items_weight, address}
SS-->>FS: label_url, tracking_number, carrier
FS->>OS: UpdateOrderStatus(order_id, DISPATCHED, tracking_number)
FS->>KF: Publish order.dispatched {order_id, tracking_number, carrier}
Warehouse Selection Algorithm
When multiple warehouses carry the same SKU, the fulfillment service picks the optimal warehouse using a scoring function:
Warehouse Score =
(0.4 × StockAvailability) -- Does this warehouse have all items?
+ (0.35 × ShippingProximity) -- How close is the warehouse to the customer?
+ (0.15 × CapacityUtilization) -- Is this warehouse too busy?
+ (0.10 × CarrierCoverage) -- Can the preferred carrier pick up from here today?
If score is tied → route to warehouse with lower current order backlog
If no single warehouse has all items → split shipment (separate orders per warehouse)
Split Shipment Handling
When no single warehouse holds all line items, the order must be split:
flowchart LR
Order["Order #12345\n- Item A (qty 2)\n- Item B (qty 1)\n- Item C (qty 3)"]
WH1["Warehouse East\nHas: Item A (5), Item C (10)"]
WH2["Warehouse West\nHas: Item B (3)"]
Order --> Split["Split Order\nEngine"]
Split --> Sub1["Sub-Order 1\nItems A + C\n→ Warehouse East"]
Split --> Sub2["Sub-Order 2\nItem B\n→ Warehouse West"]
Sub1 --> Track1["Tracking: 1Z999...001"]
Sub2 --> Track2["Tracking: 1Z999...002"]
Note["Customer sees 2 shipments\nboth linked to Order #12345"]
The parent order remains a single customer-facing entity. Sub-orders are internal records. The customer's tracking page shows all shipments under one order.
Step 9 — Shipping Integration
Carrier Integration
The Shipping Service integrates with multiple carriers through an adapter pattern — identical to the provider abstraction used in the Notification Platform.
CarrierAdapter interface:
- GenerateLabel(shipment_details) → (label_url, tracking_number, cost)
- GetTrackingStatus(tracking_number) → (status, events[], estimated_delivery)
- CancelShipment(tracking_number) → (cancelled: bool)
Implementations:
- FedExAdapter
- UPSAdapter
- DHLAdapter
- USPSAdapter
Active carrier selected by:
1. Customer preference (if premium service)
2. Lowest cost for delivery SLA
3. Carrier availability for this warehouse + destination
Tracking Event Flow
Carriers push tracking updates via webhooks. The Shipping Service receives these and updates the Order status accordingly.
sequenceDiagram
participant Carrier as FedEx Webhook
participant SS as Shipping Service
participant OS as Order Service
participant ODB as Order DB
participant KF as Kafka
participant NS as Notification Service
Carrier->>SS: POST /webhooks/fedex {tracking_number, status: "IN_TRANSIT", timestamp}
SS->>SS: Validate HMAC signature
SS->>OS: UpdateTrackingStatus(tracking_number, IN_TRANSIT)
OS->>ODB: UPDATE order SET status = IN_TRANSIT WHERE tracking_number = :tn
OS->>ODB: INSERT order_audit_event {IN_TRANSIT, timestamp, carrier_event_data}
OS->>KF: Publish order.status_changed {order_id, new_status: IN_TRANSIT}
KF->>NS: Send "Your order is on the way" push + email
Step 10 — Cancellation and Returns
Cancellation Flow
Cancellation is only allowed up to the ALLOCATED state. Once picking begins, the order cannot be cancelled through self-service — it requires operations team intervention.
flowchart TB
Cancel["Customer cancels order"]
Cancel --> CheckState{"Current order state?"}
CheckState -->|"PENDING or PAYMENT_AUTHORIZED"| EarlyCancel["Easy cancel:\n1. Void payment auth\n2. Release inventory reservation\n3. Set status = CANCELLED"]
CheckState -->|"CONFIRMED or ALLOCATED"| MidCancel["Mid-cancel:\n1. Initiate refund via Payment Service\n2. Release inventory reservation\n3. Set status = CANCELLED\n4. Notify fulfillment to abort pick task"]
CheckState -->|"PICKING or beyond"| LateCancel["Late cancel (ops only):\nOrder in physical handling — \ncannot be stopped via API\nCustomer must use returns flow after delivery"]
EarlyCancel --> Notify["Notify customer:\nCancellation confirmed + refund timeline"]
MidCancel --> Notify
LateCancel --> Notify2["Notify customer:\nCancellation cannot be processed — \naccept delivery and request return"]
Return Flow (RMA)
A return request creates a Return Merchandise Authorization (RMA) record and orchestrates the reverse logistics + refund.
sequenceDiagram
participant C as Customer
participant RS as Returns Service
participant SS as Shipping Service
participant IS as Inventory Service
participant PS as Payment Service
participant OS as Order Service
C->>RS: POST /v1/returns {order_id, line_items, reason}
RS->>OS: Verify order is in DELIVERED state
RS->>RS: Create RMA record {rma_id, items, return_window_valid}
RS->>SS: GenerateReturnLabel(rma_id, customer_address, warehouse_address)
SS-->>RS: return_label_url, return_tracking_number
RS-->>C: RMA confirmed {rma_id, return_label_url, instructions}
Note over C: Customer ships item back
SS-->>RS: Webhook: return package IN_TRANSIT
SS-->>RS: Webhook: return package DELIVERED to warehouse
RS->>IS: InspectReturn(rma_id)
Note over IS: Warehouse inspects item condition
IS-->>RS: condition=RESALABLE → restock inventory
RS->>PS: InitiateRefund(order_id, line_items, amount, idempotency_key)
PS-->>RS: refund_id, status=PROCESSING
RS->>OS: UpdateOrderStatus(order_id, REFUNDED)
RS->>KF: Publish order.refunded {order_id, refund_amount, rma_id}
Refund Policy Rules
Not all returns result in a full refund. The Returns Service applies business rules:
| Return Scenario | Refund Amount | Restocking |
|---|---|---|
| Item unopened, returned within 30 days | 100% | Yes — full restock |
| Item opened, returned within 30 days | 100% (customer goodwill) or 85% | Yes — inspect and restock |
| Item damaged by customer | 0–50% (ops decision) | No — write off |
| Item returned after 30 days | 50% store credit | Yes if resalable |
| Seller error (wrong item, defective) | 100% + return shipping reimbursed | No — returned to seller |
Step 11 — Database Design
Core Tables
-- Master order record
CREATE TABLE orders (
order_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
customer_id UUID NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'PENDING',
total_amount BIGINT NOT NULL, -- in cents
currency CHAR(3) NOT NULL DEFAULT 'USD',
shipping_address JSONB NOT NULL,
payment_intent_id VARCHAR(100), -- payment provider reference
idempotency_key VARCHAR(255) NOT NULL UNIQUE,
warehouse_id VARCHAR(50),
tracking_number VARCHAR(100),
carrier VARCHAR(20),
estimated_delivery DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Line items — one row per SKU per order
CREATE TABLE order_line_items (
line_item_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(order_id),
sku VARCHAR(50) NOT NULL,
product_name VARCHAR(255) NOT NULL,
unit_price BIGINT NOT NULL, -- in cents, snapshot at order time
quantity INT NOT NULL CHECK (quantity > 0),
reservation_id UUID NOT NULL, -- link to inventory reservation
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Immutable audit trail — one row per state transition
CREATE TABLE order_events (
event_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(order_id),
from_status VARCHAR(30),
to_status VARCHAR(30) NOT NULL,
actor VARCHAR(50) NOT NULL, -- 'customer' | 'system' | 'warehouse_op' | 'carrier'
actor_id VARCHAR(100), -- user_id or service name
metadata JSONB, -- carrier events, scan data, op notes
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Return merchandise authorization
CREATE TABLE return_requests (
rma_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
order_id UUID NOT NULL REFERENCES orders(order_id),
customer_id UUID NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'REQUESTED',
reason VARCHAR(50) NOT NULL,
return_label_url TEXT,
return_tracking VARCHAR(100),
refund_amount BIGINT,
refund_id VARCHAR(100),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Return line items
CREATE TABLE return_line_items (
return_line_id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
rma_id UUID NOT NULL REFERENCES return_requests(rma_id),
line_item_id UUID NOT NULL REFERENCES order_line_items(line_item_id),
quantity INT NOT NULL,
condition VARCHAR(20), -- 'NEW' | 'RESALABLE' | 'DAMAGED'
restocked BOOLEAN NOT NULL DEFAULT FALSE
);
Key Indexes
-- Customer order history (most common query)
CREATE INDEX idx_orders_customer_id ON orders(customer_id, created_at DESC);
-- Order lookup by tracking number (carrier webhook handler)
CREATE INDEX idx_orders_tracking ON orders(tracking_number) WHERE tracking_number IS NOT NULL;
-- Audit events per order (order timeline view)
CREATE INDEX idx_order_events ON order_events(order_id, occurred_at DESC);
-- Active reservations (expiry job scan)
CREATE INDEX idx_reservations_expiry ON inventory_reservations(expires_at)
WHERE status = 'ACTIVE';
-- Returns by order (customer service lookup)
CREATE INDEX idx_returns_order_id ON return_requests(order_id);
Read vs Write Routing
flowchart LR
OrderWrite["Order Service\n(writes — state transitions)"]
TrackAPI["Order Tracking API\n(high-read — customer polling)"]
OpsDB["Ops Dashboard\n(analytical reads)"]
Primary["PostgreSQL Primary"]
Replica1["Read Replica 1\n(tracking + status reads)"]
Replica2["Read Replica 2\n(ops dashboard + analytics)"]
Cache["Redis Cache\norder_status:{order_id}\nTTL: 30 seconds"]
TrackAPI --> Cache
Cache -->|"Cache miss"| Replica1
OrderWrite --> Primary
OpsDB --> Replica2
Primary -->|"Streaming replication"| Replica1
Primary -->|"Streaming replication"| Replica2
Note["Order status cached in Redis\nInvalidated on every state transition\n(write-through on UPDATE)"]
Step 12 — API Design
Place Order
POST /v1/orders
{
"idempotency_key": "cart-checkout-user-123-2025-03-01T10:00:00Z",
"cart_id": "cart_abc789",
"payment_method_id": "pm_card_visa_4242",
"shipping_address": {
"name": "Alex Johnson",
"line1": "123 Main St",
"city": "San Francisco",
"state": "CA",
"zip": "94105",
"country": "US"
},
"delivery_preference": "standard"
}
201 Created response:
{
"order_id": "ord_9f8e7d6c5b4a",
"status": "confirmed",
"total_amount": 8999,
"currency": "USD",
"estimated_delivery": "2025-03-05",
"line_items": [
{ "sku": "SKU-001", "name": "Wireless Headphones", "quantity": 1, "unit_price": 7999 },
{ "sku": "SKU-044", "name": "USB-C Cable", "quantity": 2, "unit_price": 500 }
],
"created_at": "2025-03-01T10:00:02Z"
}
Get Order Status
GET /v1/orders/{order_id}
{
"order_id": "ord_9f8e7d6c5b4a",
"status": "in_transit",
"tracking": {
"number": "1Z999AA10123456784",
"carrier": "UPS",
"url": "https://tracking.ups.com/track?num=1Z999AA10123456784",
"estimated_delivery": "2025-03-05"
},
"timeline": [
{ "status": "confirmed", "occurred_at": "2025-03-01T10:00:02Z" },
{ "status": "allocated", "occurred_at": "2025-03-01T10:05:00Z" },
{ "status": "picking", "occurred_at": "2025-03-01T14:12:00Z" },
{ "status": "dispatched", "occurred_at": "2025-03-01T16:30:00Z" },
{ "status": "in_transit", "occurred_at": "2025-03-02T08:45:00Z" }
]
}
Cancel Order
POST /v1/orders/{order_id}/cancel
{
"reason": "customer_requested"
}
200 OK:
{
"order_id": "ord_9f8e7d6c5b4a",
"status": "cancelled",
"refund_id": "ref_abc123",
"refund_amount": 8999,
"refund_eta": "2025-03-04"
}
Initiate Return
POST /v1/returns
{
"order_id": "ord_9f8e7d6c5b4a",
"items": [
{ "line_item_id": "li_001", "quantity": 1, "reason": "defective" }
]
}
201 Created:
{
"rma_id": "rma_xyz456",
"return_label_url": "https://returns.example.com/labels/rma_xyz456.pdf",
"return_tracking": "1Z999AA10987654321",
"instructions": "Print label and drop off at any UPS location within 14 days",
"refund_estimate": 8999,
"refund_eta_days": 5
}
Step 13 — Kafka Event Architecture
The Order Service publishes events to Kafka for every major state transition. Downstream services subscribe to relevant topics — they don't poll the Order Service.
Event Topics
| Topic | Published By | Subscribers | Payload |
|---|---|---|---|
orders.created |
Order Service | Fraud Detection, Analytics | order_id, customer_id, amount, line_items |
orders.confirmed |
Order Service | Fulfillment Service, Notification Service | order_id, line_items, address, warehouse_hint |
orders.allocated |
Fulfillment Service | Notification Service | order_id, warehouse_id |
orders.dispatched |
Fulfillment Service | Shipping Service, Notification Service | order_id, tracking_number, carrier |
orders.delivered |
Shipping Service | Notification Service, Loyalty Service | order_id, delivered_at |
orders.cancelled |
Order Service | Fulfillment Service, Notification Service | order_id, reason, refund_id |
orders.refunded |
Returns Service | Notification Service, Accounting | order_id, rma_id, refund_amount |
inventory.low_stock |
Inventory Service | Purchasing, Merchandising | sku, warehouse_id, current_qty |
Event Payload Example
{
"event_type": "orders.confirmed",
"event_id": "evt_2f3a9b2c",
"order_id": "ord_9f8e7d6c5b4a",
"customer_id": "usr_abc123",
"occurred_at": "2025-03-01T10:00:02Z",
"trace_id": "7f3a9b2c",
"payload": {
"line_items": [
{ "sku": "SKU-001", "quantity": 1, "warehouse_hint": "WH-EAST" },
{ "sku": "SKU-044", "quantity": 2, "warehouse_hint": "WH-EAST" }
],
"shipping_address": { "city": "San Francisco", "state": "CA", "zip": "94105" },
"total_amount": 8999,
"currency": "USD"
}
}
Step 14 — Failure Handling
Payment Authorization Timeout
The payment provider can time out. The order is in an ambiguous state — the provider may or may not have authorized the payment.
flowchart TB
Timeout["Payment Auth Timeout\n(no response in 5s)"]
Timeout --> Query["Query payment status\n(synchronous retry)"]
Query -->|"Auth succeeded"| Proceed["Treat as success\nProceed to capture"]
Query -->|"Auth failed"| ReleaseAndFail["Release inventory reservation\nSet order = PAYMENT_FAILED"]
Query -->|"Still no response"| Async["Schedule async reconciliation\n(check again in 60s via background job)\nSet order = PENDING_PAYMENT_VERIFICATION"]
Why not fail immediately on timeout? A network timeout between the Order Service and Payment Service does not mean the authorization failed. The provider may have processed it successfully. Failing immediately and releasing the inventory reservation means the customer is charged but the order was never confirmed — a billing error. Reconciliation prevents this.
Idempotency for Retries
Every external call from the Order Service carries an idempotency key.
| Call | Idempotency Key Construction |
|---|---|
| Reserve inventory | reserve:{order_idempotency_key}:{sku} |
| Authorize payment | auth:{order_idempotency_key} |
| Capture payment | capture:{order_id} |
| Generate shipping label | label:{order_id}:{sub_order_id} |
| Initiate refund | refund:{rma_id}:{line_item_id} |
If the Order Service crashes and restarts after sending a request but before receiving the response, it retries with the same idempotency key. The downstream service returns the same result as the original call, and no duplicate action is taken.
Fulfillment Exception Handling
A short pick occurs when a warehouse picker cannot find an item at its bin location (item lost, miscounted, or damaged). The order enters an EXCEPTION state and requires ops intervention.
| Exception Resolution | Action |
|---|---|
| Item found at alternate bin | Resume picking — update bin location in WMS |
| Item available at another warehouse | Reroute sub-order to alternate warehouse |
| Item out of stock across all warehouses | Cancel that line item; partial refund; notify customer |
| Item is a required item and no substitute | Cancel full order; full refund; notify customer with apology |
Step 15 — Observability
Key Metrics
| Metric | Description | Alert Threshold |
|---|---|---|
orders.placement_success_rate |
% orders confirmed vs attempted | < 98% |
orders.placement_latency_p99 |
e2e order placement latency | > 5 seconds |
inventory.reservation_failure_rate |
% reservations failing (out-of-stock) | Spike > 10% |
payments.authorization_failure_rate |
% payment authorizations declined | > 8% (fraud signal) |
fulfillment.allocation_lag_p99 |
Time from confirmed to allocated | > 15 minutes |
fulfillment.dispatch_sla_breach_rate |
% orders not dispatched within SLA | > 2% |
orders.cancellation_rate |
% orders cancelled post-confirmation | Spike > 5% |
returns.rma_processing_time_p99 |
Time from return received to refund issued | > 5 business days |
kafka.consumer_lag |
Lag on fulfillment + notification topics | > 10,000 messages |
Order SLA Dashboard
Every order is tracked against its fulfillment SLA. The ops team sees a real-time view of in-flight orders and their SLA status:
| SLA Stage | Target | Warning | Critical |
|---|---|---|---|
| Confirmed → Allocated | < 30 min | > 20 min | > 30 min |
| Allocated → Picking | < 2 hours | > 90 min | > 2 hours |
| Picking → Dispatched | < 4 hours | > 3 hours | > 4 hours |
| Dispatched → Delivered | Carrier SLA | Carrier SLA − 12h | Carrier SLA exceeded |
Orders approaching SLA breach are surfaced in a priority queue for ops team intervention.
Design Trade-offs
Trade-off 1: Synchronous vs Asynchronous Payment Capture
| Approach | Pros | Cons |
|---|---|---|
| Synchronous capture (chosen) — capture immediately after order record is written | Simpler flow; order is fully confirmed before returning to customer | Slightly slower API response; capture failure must be handled inline |
| Asynchronous capture — capture in background worker | Faster API response | Order appears confirmed before payment is captured; reconciliation complexity |
Decision: Capture synchronously. A "confirmed" order must mean the money was taken. Asynchronous capture creates a window where the order is confirmed but the payment has not been captured — confusing for customers and creates reconciliation debt.
Trade-off 2: Strong vs Eventual Consistency for Inventory
| Approach | Pros | Cons |
|---|---|---|
| Strong consistency (chosen) — atomic UPDATE with check constraint | Zero overselling; correct at all concurrency levels | Inventory DB is a write bottleneck under high load |
| Eventual consistency — check then reserve with async reconciliation | Higher throughput; scales reads easily | Overselling possible in race conditions; reconciliation logic is complex |
Decision: Strong consistency for inventory. Overselling is a business disaster — it means customers receive confirmations for items that don't exist. The write throughput of 60 reservations/second peak is well within PostgreSQL's capability with row-level locking.
Trade-off 3: Single Fulfillment Service vs Per-Warehouse Microservices
| Approach | Pros | Cons |
|---|---|---|
| Single Fulfillment Service (chosen) | Simpler deployment; shared routing logic; one place to change warehouse selection | Service grows large; different warehouses may have different WMS integrations |
| Per-warehouse microservices | Independent deployability; different WMS per warehouse | Massive proliferation of services; shared logic duplication; complex routing |
Decision: Single Fulfillment Service with warehouse-specific adapters, similar to the provider adapter pattern used in the Notification Service. Each WMS integration is a pluggable adapter; the routing and orchestration logic lives in one place.
Trade-off 4: Saga Choreography vs Orchestration
| Approach | Pros | Cons |
|---|---|---|
| Choreography (chosen for order creation) | No central coordinator; services are loosely coupled; fewer network hops | Harder to track saga state across services; failure handling distributed across services |
| Orchestration (for complex post-creation flows) | Central saga state; clear failure recovery; easier to monitor | Single point of failure in the orchestrator; tighter coupling |
Decision: Use choreography for the linear order creation Saga (4 steps, clear rollback path). Use event-driven choreography via Kafka for post-creation flows (fulfillment, shipping, notifications) where loose coupling is more important than central visibility.
Common Interview Mistakes
| Mistake | Why It Matters |
|---|---|
| Not defining the order state machine explicitly | Without a state machine, invalid transitions happen — orders get shipped after cancellation, refunds issued without delivery |
| Checking inventory and then writing the order in separate transactions | Creates a TOCTOU race — two customers both see "in stock", both get confirmed, one is oversold |
| Forgetting idempotency keys on payment calls | Network retries cause double charges — a critical financial bug |
| Using a single monolithic "order" table for all state | Makes it impossible to independently scale the audit trail, query timeline events, or partition by age |
| Designing fulfillment as synchronous in the order creation path | Warehouse allocation can take minutes; blocking the API on it produces 30+ second response times |
| Not modeling the audit event log as immutable | Mutable status history cannot be trusted for compliance, customer service, or debugging |
| Ignoring split shipments | A real-world OMS must handle the case where no single warehouse carries all items |
| Missing the reservation expiry job | Abandoned cart reservations permanently lock inventory, causing artificial out-of-stock errors |
Summary
The Order Management System is the central coordinating system in any e-commerce or retail operation. It does not just store orders — it orchestrates every business process that an order touches.
flowchart TB
Core["Order Management System"]
Core --> Create["Order Creation\nIdempotent checkout\nSaga: reserve → authorize → capture"]
Core --> State["State Machine\nStrict lifecycle enforcement\nImmutable audit trail"]
Core --> Fulfill["Fulfillment Pipeline\nWarehouse selection\nPick → Pack → Dispatch"]
Core --> Ship["Shipping Integration\nCarrier adapters\nWebhook-driven tracking"]
Core --> PostOrder["Post-Order\nCancellations\nReturns + Refunds"]
Core --> Observe["Observability\nSLA tracking\nOps dashboards"]
The three design principles that make an OMS robust:
-
Every state transition is a fact, not an update — the audit event log is append-only; state is derived by replaying events. You can always reconstruct exactly what happened to any order.
-
Inventory correctness is non-negotiable — the atomic reservation pattern with PostgreSQL row-level locking is the minimum bar. Overselling is not a trade-off; it is a failure.
-
Decouple acceptance from execution — the order placement API returns in under 2 seconds because fulfillment, shipping, and notification work happens asynchronously. Kafka is the glue. The API's only job is to accept, validate, and confirm — not to execute every downstream action inline.