Design an E-Commerce Platform
An e-commerce platform like Amazon handles hundreds of millions of products, processes millions of orders daily, and must provide a seamless shopping experience from product discovery to payment and delivery. This is one of the broadest system design questions because it touches on product catalogs, search, cart management, inventory, order processing, payments, and recommendations. This guide covers the full architecture for a production-grade e-commerce platform.
1. Requirements
Functional Requirements
- Product catalog: browse, search, filter, and view product details.
- Shopping cart: add, remove, update items with persistence across sessions.
- Order processing: checkout, payment, and order confirmation.
- Inventory management: real-time stock tracking, prevent overselling.
- Search with filters: keyword search, category filters, price range, ratings, sorting.
- User accounts, addresses, and order history.
- Product recommendations: personalized suggestions, "frequently bought together."
- Reviews and ratings for products.
- Seller management: product listing, order fulfillment, payouts.
Non-Functional Requirements
- High availability: 99.99% uptime, especially during sales events.
- Low latency: Product pages load in under 500ms. Search results in under 300ms.
- Scalability: Handle 100M+ DAU with 10x spikes during sales.
- Consistency: Inventory counts must be accurate to prevent overselling.
- Durability: No orders lost. Payment state must be correct.
2. Capacity Estimation
| Metric | Estimate |
|---|---|
| Daily Active Users | 100 million |
| Total products in catalog | 500 million |
| Product page views per day | 2 billion |
| Search queries per day | 500 million |
| Orders per day | 5 million |
| Cart operations per day | 50 million (add/remove/update) |
| Peak orders per second (flash sale) | ~50,000/sec (10x normal) |
| Product data per item | ~5 KB (title, description, specs, pricing) |
| Total catalog storage | 500M × 5 KB = 2.5 TB |
3. High-Level Design
| Service | Responsibility |
|---|---|
| Product Catalog Service | Product CRUD, categories, attributes |
| Search Service | Full-text search, faceted filtering, ranking |
| Cart Service | Shopping cart management |
| Order Service | Order creation, status tracking, fulfillment |
| Inventory Service | Stock levels, reservation, decrement |
| Payment Service | Payment processing, refunds |
| User Service | Accounts, addresses, authentication |
| Recommendation Engine | Personalized product suggestions |
| Review Service | Product reviews and ratings |
| Notification Service | Order updates, shipping alerts |
| CDN | Serves product images and static assets |
| Cache Layer | Redis for product data, session, cart |
4. Detailed Component Design
4.1 Product Catalog
The product catalog is read-heavy (2B page views/day vs infrequent updates). Use a relational database as the source of truth with aggressive caching.
// Product page data assembly
async function getProductPage(productId) {
// L1: Local in-memory cache (1 min TTL)
let product = localCache.get(productId);
if (product) return product;
// L2: Redis cache (1 hour TTL)
product = await redis.get(`product:${productId}`);
if (product) {
localCache.set(productId, product, ttl: 60);
return JSON.parse(product);
}
// L3: Database
product = await db.query(`SELECT * FROM products WHERE id = ?`, productId);
const enriched = {
...product,
reviews: await reviewService.getSummary(productId),
recommendations: await recoService.getSimilar(productId, 10),
inventory: await inventoryService.getAvailability(productId)
};
await redis.setex(`product:${productId}`, 3600, JSON.stringify(enriched));
return enriched;
}
4.2 Shopping Cart
The cart must persist across sessions and devices. Two approaches:
| Approach | Pros | Cons |
|---|---|---|
| Database-backed cart | Persistent, durable, cross-device | Higher latency for frequent cart operations |
| Redis-backed cart | Fast reads/writes, handles high throughput | Risk of data loss on Redis failure (mitigated with persistence) |
Recommended: Use Redis as the primary cart store with async persistence to the database. Redis Hashes work well for carts:
// Cart operations using Redis Hash
// Key: cart:{userId}, Field: productId, Value: JSON(quantity, addedAt, price)
async function addToCart(userId, productId, quantity) {
const product = await catalogService.getProduct(productId);
const stock = await inventoryService.checkStock(productId);
if (stock < quantity) throw new Error("Insufficient stock");
const item = JSON.stringify({
quantity: quantity,
price: product.price,
name: product.name,
addedAt: Date.now()
});
await redis.hset(`cart:${userId}`, productId, item);
await redis.expire(`cart:${userId}`, 2592000); // 30 day TTL
}
async function getCart(userId) {
const items = await redis.hgetall(`cart:${userId}`);
const cartItems = [];
for (const [productId, data] of Object.entries(items)) {
const item = JSON.parse(data);
// Re-validate price and stock at read time
const current = await catalogService.getProduct(productId);
cartItems.push({
productId,
quantity: item.quantity,
currentPrice: current.price,
originalPrice: item.price,
priceChanged: current.price !== item.price,
inStock: await inventoryService.checkStock(productId) >= item.quantity
});
}
return cartItems;
}
4.3 Order Processing and Checkout
Checkout is the most critical flow. It must be reliable, consistent, and handle concurrent inventory updates.
async function checkout(userId, paymentMethodId, shippingAddressId) {
const cart = await getCart(userId);
if (cart.length === 0) throw new Error("Cart is empty");
// Step 1: Reserve inventory for all items (atomic)
const reservationId = await inventoryService.reserveItems(
cart.map(item => ({ productId: item.productId, quantity: item.quantity })),
reservationTTL: 600 // 10 minute hold
);
try {
// Step 2: Calculate totals
const subtotal = cart.reduce((sum, item) => sum + item.currentPrice * item.quantity, 0);
const tax = calculateTax(subtotal, shippingAddressId);
const shipping = calculateShipping(cart, shippingAddressId);
const total = subtotal + tax + shipping;
// Step 3: Create order (status: pending_payment)
const order = await orderService.create({
userId, items: cart, subtotal, tax, shipping, total,
shippingAddressId, status: 'pending_payment',
reservationId
});
// Step 4: Process payment
const payment = await paymentService.charge({
userId, amount: total, orderId: order.id,
paymentMethodId, idempotencyKey: `order_${order.id}`
});
// Step 5: Confirm order
await orderService.updateStatus(order.id, 'confirmed');
await inventoryService.confirmReservation(reservationId);
await redis.del(`cart:${userId}`);
// Step 6: Trigger async post-checkout tasks
await eventBus.publish('order.confirmed', { orderId: order.id });
return order;
} catch (error) {
// Release inventory reservation on failure
await inventoryService.releaseReservation(reservationId);
throw error;
}
}
4.4 Inventory Management
Preventing overselling is critical. Use a reservation-based approach:
-- Inventory table with available and reserved counts
CREATE TABLE inventory (
product_id BIGINT PRIMARY KEY,
total_stock INT NOT NULL DEFAULT 0,
reserved INT NOT NULL DEFAULT 0,
available INT GENERATED ALWAYS AS (total_stock - reserved) STORED,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Reserve inventory (within a transaction)
BEGIN;
UPDATE inventory
SET reserved = reserved + :quantity,
updated_at = NOW()
WHERE product_id = :productId
AND (total_stock - reserved) >= :quantity;
-- If 0 rows affected: insufficient stock
INSERT INTO reservations (id, product_id, quantity, expires_at)
VALUES (:reservationId, :productId, :quantity, NOW() + INTERVAL '10 MINUTE');
COMMIT;
-- Confirm reservation (convert to actual decrement)
BEGIN;
UPDATE inventory SET total_stock = total_stock - :quantity, reserved = reserved - :quantity
WHERE product_id = :productId;
DELETE FROM reservations WHERE id = :reservationId;
COMMIT;
-- Background job: release expired reservations
UPDATE inventory SET reserved = reserved - r.quantity
FROM reservations r
WHERE inventory.product_id = r.product_id AND r.expires_at < NOW();
4.5 Search
Product search uses Elasticsearch with faceted filtering:
// Elasticsearch query for "wireless headphones" with filters
{
"query": {
"bool": {
"must": [
{ "multi_match": {
"query": "wireless headphones",
"fields": ["title^3", "description", "brand^2", "category"]
}}
],
"filter": [
{ "range": { "price": { "gte": 20, "lte": 200 } } },
{ "term": { "in_stock": true } },
{ "range": { "avg_rating": { "gte": 4.0 } } }
]
}
},
"aggs": {
"brands": { "terms": { "field": "brand.keyword", "size": 20 } },
"price_ranges": { "range": { "field": "price", "ranges": [
{ "to": 25 }, { "from": 25, "to": 50 },
{ "from": 50, "to": 100 }, { "from": 100 }
]}},
"avg_rating": { "range": { "field": "avg_rating", "ranges": [
{ "from": 4 }, { "from": 3 }, { "from": 2 }
]}}
},
"sort": [
{ "_score": "desc" },
{ "sales_count": "desc" }
]
}
4.6 Recommendations
Product recommendations drive significant revenue. Key algorithms:
- Collaborative filtering: "Customers who bought X also bought Y." Based on purchase co-occurrence matrices.
- Content-based: Similar products by category, attributes, brand.
- Frequently bought together: Products often in the same order.
- Personalized homepage: Based on browsing history, purchase history, and user segment.
5. Database Schema
CREATE TABLE products (
id BIGINT PRIMARY KEY,
seller_id BIGINT NOT NULL,
title VARCHAR(500) NOT NULL,
description TEXT,
category_id BIGINT,
brand VARCHAR(100),
price_cents BIGINT NOT NULL,
original_price_cents BIGINT,
currency VARCHAR(3) DEFAULT 'USD',
avg_rating DECIMAL(3,2) DEFAULT 0,
review_count INT DEFAULT 0,
status ENUM('active','inactive','deleted') DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_products_category ON products(category_id, status);
CREATE INDEX idx_products_seller ON products(seller_id);
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
status ENUM('pending_payment','confirmed','processing','shipped',
'delivered','cancelled','refunded') DEFAULT 'pending_payment',
subtotal_cents BIGINT NOT NULL,
tax_cents BIGINT NOT NULL,
shipping_cents BIGINT NOT NULL,
total_cents BIGINT NOT NULL,
shipping_address_id BIGINT NOT NULL,
payment_id VARCHAR(36),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_orders_user ON orders(user_id, created_at DESC);
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL REFERENCES orders(id),
product_id BIGINT NOT NULL,
seller_id BIGINT NOT NULL,
quantity INT NOT NULL,
unit_price_cents BIGINT NOT NULL,
status ENUM('pending','shipped','delivered','returned') DEFAULT 'pending'
);
CREATE INDEX idx_order_items_order ON order_items(order_id);
CREATE TABLE reviews (
id BIGINT PRIMARY KEY,
product_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
title VARCHAR(200),
body TEXT,
helpful_count INT DEFAULT 0,
verified_purchase BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (user_id, product_id, order_id)
);
CREATE INDEX idx_reviews_product ON reviews(product_id, created_at DESC);
6. Key Trade-offs
| Decision | Trade-off |
|---|---|
| Inventory: pessimistic vs optimistic locking | Pessimistic (SELECT FOR UPDATE) prevents overselling but reduces throughput. Optimistic (version column) allows higher throughput but requires retry logic. For limited-stock items (flash sales), use pessimistic locking. For normal inventory, optimistic with retry. |
| Cart storage: Redis vs database | Redis is faster (sub-ms reads) but less durable. Database is more durable but slower. Hybrid (Redis primary, async DB backup) provides both speed and durability. |
| Monolith vs microservices | Monolith is simpler for small teams. Microservices enable independent scaling (search scales differently from orders) and team autonomy. At e-commerce scale, microservices are necessary. |
| Search: real-time vs batch indexing | Real-time indexing ensures new products appear immediately but adds write load. Batch indexing is cheaper but delays product visibility. Use near-real-time (sub-minute) indexing via change data capture from the product database. |
7. Scaling Considerations
7.1 Flash Sale Handling
Flash sales generate 10-50x normal traffic in minutes. Strategies: (1) Pre-warm caches with sale products. (2) Use a queue for checkout requests during peak (rather than dropping users). (3) Pre-generate inventory reservation tokens. (4) Use a CDN for static product pages. (5) Auto-scale all services 30 minutes before sale.
7.2 Database Sharding
Shard products by product_id (or category_id for range queries). Shard orders by user_id. Shard inventory by product_id. See sharding strategies. Use consistent hashing for shard assignment.
7.3 Search Scaling
With 500M products, the Elasticsearch cluster needs careful sizing. Shard the index by category or product_id range. Use time-based indices for review data. Deploy search nodes in multiple regions with load balancing.
7.4 Global Deployment
Deploy in multiple regions for low latency. Product catalog and search can be replicated globally (read-heavy, eventually consistent). Orders and payments should be processed in the user's region with strong consistency. Understand the CAP theorem implications.
Use swehelper.com tools to practice e-commerce system design and capacity estimation.
8. Frequently Asked Questions
Q1: How do you prevent overselling during flash sales?
Use a reservation-based inventory system with pessimistic locking. When a user adds an item to cart during a flash sale, immediately reserve the inventory with a short TTL (10 minutes). If the user does not checkout within the TTL, the reservation expires and the stock becomes available again. For extremely hot items, use Redis atomic decrements (DECRBY) for the fastest possible stock checks, backed by database reconciliation.
Q2: How does the checkout flow handle payment failures?
The checkout creates an order in "pending_payment" status and reserves inventory. If payment fails: (1) The order status is updated to "payment_failed." (2) Inventory reservation is released. (3) The user is prompted to retry with a different payment method. The order and reservation IDs are preserved so the user does not need to re-add items. The idempotency key ensures retrying payment does not create a duplicate charge.
Q3: How does product search handle 500 million products efficiently?
Elasticsearch indexes products with sharding across multiple nodes. Product data is indexed with weighted fields (title has 3x weight, brand has 2x). Faceted search uses Elasticsearch aggregations for dynamic filter counts. Results are pre-scored by relevance, sales velocity, and rating. For popular queries, results are cached in Redis for sub-10ms response times. The search index is updated in near-real-time via change data capture from the product database.
Q4: How do product recommendations work at scale?
Recommendations are pre-computed in batch (offline) and served from cache. Collaborative filtering builds a co-purchase matrix: for every product pair (A, B), count how many orders contain both. These counts are normalized and stored. "Frequently bought together" uses the same matrix. For personalized homepage recommendations, user browsing/purchase history is matched against product feature vectors. The results are cached per-user with hourly refresh. Real-time click signals can adjust rankings within the cached set.