What Is an E-Commerce Order System?
An e-commerce order system manages the full lifecycle of an online purchase: cart management, checkout, inventory reservation, payment processing, and order fulfillment. Examples: Amazon Orders, Shopify checkout, eBay purchases. Core challenges: concurrent inventory management (two buyers purchasing the last item), payment atomicity, and order state machine across distributed services.
System Requirements
Functional
- Shopping cart: add/remove items, persist across sessions
- Checkout: reserve inventory, compute total, process payment
- Order management: view, cancel, track fulfillment status
- Inventory management: track stock, prevent overselling
Non-Functional
- 100M users, 10K checkouts/second at peak (Black Friday)
- No overselling: never confirm an order when stock is 0
- Payment atomicity: charge once per order
Core Data Model
products: id, name, price_cents, sku
inventory: product_id, warehouse_id, quantity_available, quantity_reserved
carts: id, user_id, created_at, updated_at
cart_items: cart_id, product_id, quantity, price_at_add
orders: id, user_id, status, subtotal, tax, shipping, total, created_at
order_items: order_id, product_id, quantity, unit_price
payments: id, order_id, amount, status, idempotency_key, provider_charge_id
Cart Implementation
Carts need fast read/write and do not require ACID guarantees. Store in Redis (TTL 30 days):
HSET cart:{user_id} product_id:123 quantity:2
HSET cart:{user_id} product_id:456 quantity:1
EXPIRE cart:{user_id} 2592000 # 30 days
On checkout: read the cart from Redis, validate prices against current product prices (prices may have changed), then persist as an order in the DB.
Checkout Flow — Preventing Overselling
BEGIN TRANSACTION;
-- 1. Lock inventory rows for all items in the cart
SELECT quantity_available, quantity_reserved
FROM inventory
WHERE product_id IN (123, 456) AND warehouse_id = ?
FOR UPDATE;
-- 2. Check each product has sufficient quantity
-- (quantity_available - quantity_reserved >= ordered_quantity)
-- 3. Reserve inventory (not yet deducted -- reserved until fulfilled)
UPDATE inventory
SET quantity_reserved = quantity_reserved + ordered_qty
WHERE product_id = ?;
-- 4. Create order record
INSERT INTO orders (...) VALUES (...);
INSERT INTO order_items (...) VALUES (...);
COMMIT;
If any product check fails: rollback with an “out of stock” error. The transaction ensures atomicity — two concurrent checkouts for the last item both acquire the lock; one succeeds, the other gets the updated (insufficient) quantity and rolls back.
Payment Processing
After inventory is reserved, charge the payment method:
charge = stripe.charge.create(
amount=total_cents,
customer=stripe_customer_id,
idempotency_key=f"order-{order_id}"
)
On payment success: update order status to CONFIRMED, send confirmation email. On payment failure: release the reserved inventory (decrement quantity_reserved), update order status to PAYMENT_FAILED.
Order State Machine
PENDING_PAYMENT → CONFIRMED → PROCESSING → SHIPPED → DELIVERED
│ │
PAYMENT_FAILED CANCELLED (before shipment)
Inventory at Scale
Sharding inventory by product_id distributes write load. But for flash sales (Nike shoe drop: 10K buyers competing for 1K pairs), the single-product inventory row becomes a hotspot with thousands of concurrent locks. Solutions:
- Inventory partitioning: split 1K units across 10 virtual warehouse rows (100 units each). Random assignment distributes lock contention 10x.
- Pre-sale reservation: allow users to queue, assign inventory in batches every 30 seconds.
- Optimistic locking with retry: use version column instead of FOR UPDATE; retry on conflict (works for low contention, not flash sales).
Interview Tips
- Separate cart (Redis) from orders (SQL) — different consistency requirements.
- quantity_reserved vs quantity_available prevents deducting before payment confirms.
- Idempotency key = order_id ensures exactly-one charge.
- Inventory partitioning for flash sales shows awareness of hotspot problems.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “How do you prevent overselling when two users buy the last item simultaneously?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Overselling occurs when two transactions both read available quantity = 1, both check it is sufficient, and both decrement — resulting in quantity = -1 and two confirmed orders for one item. Prevention: use SELECT FOR UPDATE inside a database transaction to acquire exclusive row locks on the inventory rows before checking availability. Transaction A runs SELECT quantity FROM inventory WHERE product_id = ? FOR UPDATE. It acquires the lock and reads quantity = 1. Transaction B attempts the same SELECT FOR UPDATE and blocks — it must wait. A decrements quantity to 0 and commits. B now acquires the lock, reads quantity = 0, detects insufficient stock, and rolls back with an out-of-stock error to the user. Granularity: FOR UPDATE locks at the row level (product level), not the table level — concurrent purchases of different products proceed without blocking each other. Alternative for very high contention (flash sales): pre-sell tokens (N tokens for N units, claim a token atomically via DECR in Redis), then process orders asynchronously against claimed tokens.” }
},
{
“@type”: “Question”,
“name”: “What is the difference between quantity_available and quantity_reserved in inventory?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Using a single quantity column creates a window where inventory is deducted before payment confirms. If payment fails after deduction, you must increment it back — with the risk of double-incrementing on retries. The two-column model separates concerns: quantity_available is the total physical stock. quantity_reserved tracks units held for pending orders (payment not yet confirmed). Available to purchase = quantity_available – quantity_reserved. On checkout: increment quantity_reserved (the item is "spoken for"). On payment success: decrement quantity_available (the item is physically going to the customer). On payment failure: decrement quantity_reserved (release the hold). This way: (1) available_count correctly reflects what other customers can buy during the payment window; (2) double-payment safety — if the payment webhook fires twice, decrementing quantity_available a second time would undercount, so add an idempotency check; (3) cancellations after shipment only decrement quantity_available, since quantity_reserved was already decremented at payment time.” }
},
{
“@type”: “Question”,
“name”: “How do you handle the shopping cart at scale when users have millions of active carts?”,
“acceptedAnswer”: { “@type”: “Answer”, “text”: “Shopping carts have different characteristics from orders: they change frequently (add/remove items), do not require ACID guarantees (losing a cart is annoying but not a financial error), and have a natural expiry (30 days of inactivity). Redis is the standard cart store. Key: cart:{user_id} as a Redis hash where field = product_id and value = quantity. Operations: HSET, HGET, HDEL, HGETALL. EXPIRE cart:{user_id} 2592000 (30 days) resets on each update. For 100M active carts at ~500 bytes per cart: 50 GB — fits in a Redis cluster. Durability: Redis AOF (append-only file) provides durability with 1-second granularity. Periodic snapshots to RDB for recovery. On checkout: read the cart, validate each item (price may have changed since added — show user if price increased), then create an order. After successful order creation: DEL cart:{user_id}. Guest carts: use session_id as key; on login, merge guest cart with user cart (taking higher quantity for conflicts).” }
}
]
}