An e-commerce platform touches nearly every backend concept that appears in system design interviews: relational data modeling, inventory consistency, distributed payments, search, and caching. This post builds a low-level design from the core entities outward.
Core Entities
Product:
product_id UUID PK
name String
description Text
category_id FK
brand_id FK
created_at Timestamp
SKU (Stock Keeping Unit):
sku_id UUID PK
product_id FK
attributes JSONB # {"color": "red", "size": "M"}
base_price Decimal
sale_price Decimal nullable
sale_ends_at Timestamp nullable
Inventory:
sku_id FK PK
warehouse_id FK PK
quantity_on_hand Int
quantity_reserved Int # items in pending orders
Order:
order_id UUID PK
user_id FK
status Enum # PENDING | CONFIRMED | SHIPPED | DELIVERED | CANCELLED | FAILED
total_amount Decimal
created_at Timestamp
OrderItem:
order_id FK
sku_id FK
quantity Int
unit_price Decimal # snapshot of price at order time
Payment:
payment_id UUID PK
order_id FK
idempotency_key String UNIQUE
provider Enum # STRIPE | PAYPAL
status Enum # INITIATED | SUCCESS | FAILED | REFUNDED
amount Decimal
Cart Design
Carts are ephemeral and read-heavy. Store them in Redis rather than the primary DB:
Key: cart:{user_id}
Type: Redis Hash
Field: {sku_id}
Value: {quantity}
TTL: 30 days (reset on every write)
On checkout, the cart contents are read from Redis, validated (prices, availability), and written to the Orders table. The Redis cart is deleted after a successful order creation. For guest carts, use a session token as the key and migrate it to the user’s key on login.
Inventory: Reservation and Locking
Race conditions in inventory are a classic interview topic. The safe approach is pessimistic locking during reservation:
BEGIN TRANSACTION;
SELECT quantity_on_hand, quantity_reserved
FROM Inventory
WHERE sku_id = ? AND warehouse_id = ?
FOR UPDATE; -- row-level lock
available = quantity_on_hand - quantity_reserved;
IF available < requested_qty THEN
ROLLBACK; RAISE InsufficientInventory;
END IF;
UPDATE Inventory
SET quantity_reserved = quantity_reserved + requested_qty
WHERE sku_id = ? AND warehouse_id = ?;
COMMIT;
The reservation is released (decremented) on order cancellation or payment failure, and converted to a deduction from quantity_on_hand on shipment.
Order Flow
- Client calls
POST /orders→ server creates order in PENDING state. - Inventory reservation runs inside a DB transaction (see above).
- Server calls payment provider with an idempotency key =
order_id. - Payment provider webhook fires:
POST /webhooks/payment. - On
SUCCESS: order → CONFIRMED. OnFAILED: order → FAILED, inventory reservation released.
Payment Idempotency
Network timeouts can cause the client to retry a payment request. Without an idempotency key, the user gets charged twice. Use order_id as the idempotency key passed to the payment provider (Stripe supports this natively). On the webhook side, use payment_id as a unique constraint in the Payments table — a second webhook delivery for the same event is a no-op.
Product Search with Elasticsearch
Index: products
Fields:
product_id, name (text + keyword), description (text),
category (keyword), brand (keyword), tags (keyword[]),
price_min (float), # min SKU price
price_max (float), # max SKU price
in_stock (boolean),
rating (float)
Query example (faceted search):
{
"query": {
"bool": {
"must": [{"match": {"name": "running shoes"}}],
"filter": [
{"term": {"in_stock": true}},
{"range": {"price_min": {"gte": 50, "lte": 150}}},
{"term": {"category": "footwear"}}
]
}
},
"aggs": {
"brands": {"terms": {"field": "brand"}},
"price_ranges": {"range": {"field": "price_min", "ranges": [...]}}
}
}
Pricing: Sale Prices and B2B Tiers
Resolve effective price at read time:
def effective_price(sku, user):
if user.tier == "WHOLESALE":
return sku.wholesale_price
if sku.sale_price and now() < sku.sale_ends_at:
return sku.sale_price
return sku.base_price
Always snapshot the resolved price into OrderItem.unit_price at order creation time. Never recalculate from the SKU after the fact — prices change.
Key APIs
GET /products?q=&category=&brand=&price_min=&price_max=&page=
GET /products/{product_id}
POST /cart/items body: {sku_id, quantity}
DELETE /cart/items/{sku_id}
POST /orders body: {shipping_address_id, payment_method_id}
GET /orders/{order_id}
POST /webhooks/payment (called by payment provider)
Shopify system design interviews cover e-commerce platform architecture. See common questions for Shopify interview: e-commerce platform and product catalog system design.
Amazon system design covers e-commerce catalog, inventory, and orders. Review design patterns for Amazon interview: e-commerce inventory and order system design.
Stripe system design interviews cover payment flows in e-commerce. See design patterns for Stripe interview: payment processing and order system design.