Full Stack • Java • System Design • Cloud • AI Engineering

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:

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

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

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