Amazon E-Commerce System Design — 1 Hour Interview Guide
Design a scalable e-commerce platform like Amazon. Covers product catalog, search ranking, shopping cart, inventory management, checkout with Saga pattern, flash sale handling, order state machine, recommendations, database design, caching, and failure scenarios — all in a 1-hour interview format.
1-Hour Interview Roadmap
| Time | Topic |
|---|---|
| 0 – 5 min | Requirements clarification |
| 5 – 10 min | Capacity estimation |
| 10 – 18 min | High-level architecture + core components |
| 18 – 26 min | Product discovery — search, CDN, caching |
| 26 – 34 min | Shopping cart + inventory check |
| 34 – 44 min | Checkout flow — Saga pattern, inventory reservation, payment |
| 44 – 51 min | Database design + schema |
| 51 – 56 min | Flash sales — request queuing, Redis tokens, graceful degradation |
| 56 – 60 min | Trade-offs + failure scenarios |
What Are We Building?
An e-commerce platform where users can:
- Browse and search a catalog of 100 million+ products
- View product details, images, reviews, and pricing
- Add items to a shopping cart and manage quantities
- Complete purchases reliably with inventory guarantees
- Track order status from confirmation through delivery
Scale reference: Amazon serves 300+ million active customers, processes ~1.6 million orders per day, handles 23,000+ product page requests per second on a normal day, and peaks at 10x+ that volume during Prime Day and Black Friday.
Key unique challenges:
- Overselling prevention — inventory race conditions at flash sale scale (thousands of users per second per item)
- Read/write asymmetry — 1,000 reads for every 1 write; each path needs separate optimization
- Checkout reliability — payment + inventory + order creation must be atomic without distributed transactions
- Flash sales — 10x traffic spike, all concentrated on a handful of hot items
Step 1 — Requirements
Functional Requirements
| # | Requirement |
|---|---|
| 1 | Users can search for products by keyword and filter by category, price, rating |
| 2 | Users can view full product details — images, description, specs, reviews, price |
| 3 | Users can add, update, and remove items from a shopping cart |
| 4 | Cart persists across sessions and across devices for logged-in users |
| 5 | System prevents overselling — no user can purchase items that are out of stock |
| 6 | Users can complete checkout — inventory reservation, payment, order creation |
| 7 | Users can track order status from confirmation to delivery |
| 8 | System handles 10x traffic spikes during flash sales and promotional events |
| 9 | Sellers can update product details, pricing, and inventory |
| 10 | System sends order confirmations and shipping notifications |
Non-Functional Requirements
| # | Requirement |
|---|---|
| 1 | Search and product pages load within 200ms (p99) |
| 2 | High availability — 99.99% uptime |
| 3 | Strongly consistent inventory and orders — overselling is unacceptable |
| 4 | Order and payment data must never be lost (durability) |
| 5 | System scales to 10x normal traffic during peak events |
| 6 | Eventual consistency acceptable for reviews, recommendations, analytics |
Out of Scope
- Seller portal and onboarding
- Warehouse management and logistics
- Payment processor internals (assume Stripe/PayPal integration)
- Returns and refunds processing
- Fraud detection engine
Step 2 — Capacity Estimation
Assumptions
| Metric | Value |
|---|---|
| Daily Active Users (DAU) | 100 million |
| Products in catalog | 100 million |
| Product page views per user/day | ~20 |
| Orders per day (2% conversion) | 2 million |
| Cart operations per user/day | ~3 |
| Peak traffic multiplier (Prime Day) | 10x |
Traffic Estimates
Read traffic (product views + search):
100M DAU × 20 page views / 86,400 sec = ~23,000 QPS average
Peak (10x): = ~230,000 QPS
Write traffic (orders):
2M orders / 86,400 sec = ~23 QPS average
Peak (10x): = ~230 QPS
Cart operations:
100M DAU × 3 actions / 86,400 sec = ~3,500 QPS
Read : Write ratio ≈ 1,000 : 1
Storage Estimates
| Data Type | Calculation | Storage |
|---|---|---|
| Product metadata | 100M × 10 KB | ~1 TB |
| Product images | 100M × 5 images × 500 KB | ~250 TB |
| Orders (1 year) | 2M/day × 2 KB × 365 days | ~1.5 TB |
| User data | 500M users × 1 KB | ~500 GB |
Key Insights
- 1,000:1 read/write ratio — invest heavily in caching; most requests should never touch the primary database
- Images dominate storage — product images (~250 TB) live in object storage + CDN, not in the application database
- Peak traffic is the real challenge — designing for average load causes failures during the most critical moments
- Orders need special care — low volume but involve multiple services that must coordinate atomically
Step 3 — High-Level Architecture
flowchart TD
Browser[Web Browser] --> CDN[CDN\nStatic Assets + Images]
Mobile[Mobile App] --> LB[Load Balancer]
Browser --> LB
LB --> AG[API Gateway\nAuth + Rate Limiting]
AG --> SRCH[Search Service]
AG --> PROD[Product Service]
AG --> CART[Cart Service]
AG --> ORD[Order Service]
AG --> INV[Inventory Service]
ORD --> PAY[Payment Service]
ORD --> KF[Kafka\nOrderCreated]
KF --> NOTIF[Notification Service]
KF --> FULFILL[Fulfillment Service]
SRCH --> ES[(Elasticsearch\nProduct Index)]
PROD --> REDIS[(Redis\nProduct Cache)]
PROD --> PDB[(DynamoDB\nProduct Catalog)]
CART --> CARTDB[(Redis\nCart Store)]
INV --> INVDB[(PostgreSQL\nInventory)]
ORD --> ORDDB[(PostgreSQL\nOrders)]
PAY --> STRIPE[Stripe / PayPal]
Component Responsibilities
| Component | Responsibility |
|---|---|
| CDN | Serve static assets (images, JS, CSS) from global edge nodes |
| API Gateway | JWT auth, rate limiting, routing |
| Search Service | Keyword search, filters, ranking via Elasticsearch |
| Product Service | Serve product catalog — details, pricing, availability |
| Cart Service | Add/update/remove items; persist across sessions |
| Inventory Service | Track stock levels; reserve during checkout; prevent oversell |
| Order Service | Orchestrate checkout — reserve → pay → create order |
| Payment Service | Charge customer via Stripe/PayPal; handle idempotency |
| Notification Service | Send order confirmation emails and shipping updates |
| Fulfillment Service | Notify warehouse to pick, pack, and ship |
Step 4 — Product Discovery
Search Flow
sequenceDiagram
participant User
participant AG as API Gateway
participant SRCH as Search Service
participant REDIS as Redis Cache
participant ES as Elasticsearch
participant CDN
User->>AG: GET /products/search?q=wireless+headphones&max_price=150
AG->>SRCH: Forward request
SRCH->>REDIS: Check cache for query hash
REDIS-->>SRCH: Cache MISS
SRCH->>ES: multi-match query + filters + ranking
ES-->>SRCH: Top 50 ranked results (20ms)
SRCH->>REDIS: Cache results (TTL: 5 min)
SRCH-->>User: Product list {id, name, price, rating, thumbnail_url}
User->>CDN: GET product thumbnail images
CDN-->>User: Images from nearest edge node
Product Detail Flow
sequenceDiagram
participant User
participant PROD as Product Service
participant REDIS as Redis Cache
participant DDB as DynamoDB
User->>PROD: GET /products/{product_id}
PROD->>REDIS: GET product:{product_id}
REDIS-->>PROD: Cache HIT (< 1ms) or MISS
alt Cache MISS
PROD->>DDB: GetItem by product_id
DDB-->>PROD: Product document
PROD->>REDIS: SET product:{product_id} TTL=1hr
end
PROD-->>User: Full product {description, images, specs, price, stock_status}
Elasticsearch — What We Index
Each product document in Elasticsearch contains:
{
"product_id": "prod_abc123",
"title": "Sony WH-1000XM5 Wireless Headphones",
"description": "Industry-leading noise cancelling...",
"brand": "Sony",
"category_id": "electronics/headphones",
"price": 349.99,
"avg_rating": 4.7,
"review_count": 12450,
"in_stock": true,
"sales_30d": 8200,
"click_rate": 0.082,
"attributes": {
"connectivity": "Bluetooth 5.2",
"battery_life": "30 hours",
"noise_cancelling": true
}
}
Search Ranking Formula
final_score =
BM25(query_text, title, description, brand) ← text relevance
× log(sales_30d + 1) ← popularity signal
× click_through_rate ← engagement signal
× business_boost ← sponsored / Prime eligible
× personalization_factor(user_history) ← user-specific boost
- BM25 — Elasticsearch's default relevance scorer; title matches weighted 3× over description
- Popularity — products that sell well are probably correct matches for similar queries
- Personalization — user's category preferences and purchase history boost relevant results
- Business rules — sponsored products get a configurable boost; out-of-stock products are demoted
Keeping the Index Fresh
flowchart LR
PDB[(DynamoDB\nProduct Catalog)] --> CDC[CDC / Debezium\nChange Data Capture]
CDC --> KF[Kafka\nproduct.updated]
KF --> IDX[Indexer Service]
IDX --> ES[Elasticsearch\nProduct Index]
- CDC (Change Data Capture) watches the database transaction log for product updates
- Changes flow via Kafka to the Indexer Service which updates Elasticsearch
- Typical lag: 1–5 seconds — acceptable for price changes to appear in search
CDN Strategy
| Asset | TTL | Reason |
|---|---|---|
| Product images | 30 days | Rarely change after first upload |
| Category page HTML | 5 min | Mostly static; price/stock can be stale |
| Search results | 5 min | Common queries repeated by many users |
| Product detail page | 1 hour | Price and availability update infrequently |
Product images are the heaviest asset — 5 images × 500 KB = 2.5 MB per product. Serving 23,000+ images/second from origin is economically and technically infeasible. CDN edge caching is mandatory.
Step 5 — Shopping Cart
Cart Architecture
sequenceDiagram
participant User
participant CART as Cart Service
participant INV as Inventory Service
participant REDIS as Redis (Cart)
participant CARTDB as Cart DB (PostgreSQL)
User->>CART: POST /cart/items {product_id: "prod_abc", quantity: 2}
CART->>INV: GET /inventory/{product_id}/available
INV-->>CART: {available_qty: 156, in_stock: true}
CART->>REDIS: HSET cart:{user_id} prod_abc {"qty":2, "price":349.99}
REDIS-->>CART: OK (< 1ms)
CART-->>User: {cart: [{product_id, qty, subtotal}], total: $699.98}
Note over CART,CARTDB: Async persist for logged-in users (cross-device sync)
CART->>CARTDB: UPSERT cart_items (async)
Design Decisions
We do NOT reserve inventory when adding to cart. Adding an item to a cart is just a read check. Actual inventory reservation only happens at checkout.
Why? Cart abandonment rate is ~70%. If we reserved inventory at cart-add time, popular items would appear "out of stock" even though most of that reserved stock would be released when users abandon their carts.
Guest carts are stored in Redis with a 48-hour TTL keyed by a session cookie. On login, the guest cart is merged with the user's account cart.
Redis Cart Structure
Key: cart:{user_id}
Type: Hash
Fields:
{product_id} → {qty, price_at_add, added_at}
TTL: 30 days for logged-in users; 48 hours for guests
Step 6 — Checkout and Order Processing
Full Checkout Sequence (Saga Pattern)
sequenceDiagram
participant User
participant AG as API Gateway
participant ORD as Order Service
participant INV as Inventory Service
participant PAY as Payment Service
participant ORDDB as Order DB
participant KF as Kafka
User->>AG: POST /orders {cart_id, address, payment_method}
AG->>ORD: Create order (idempotency_key: uuid)
Note over ORD: Step 1 — Reserve Inventory
ORD->>INV: Reserve items for cart (TTL: 15 min)
INV-->>ORD: reservation_id (or 409 Out of Stock)
Note over ORD: Step 2 — Process Payment
ORD->>PAY: Charge {amount, card_token, idempotency_key}
PAY-->>ORD: payment_id (or 402 Payment Failed)
Note over ORD: Step 3 — Create Order Record
ORD->>ORDDB: INSERT order {status: CONFIRMED, items, payment_id}
ORDDB-->>ORD: order_id
Note over ORD: Step 4 — Confirm & Publish
ORD->>INV: Confirm reservation → deduct stock permanently
ORD->>KF: Publish order.created {order_id, user_id, items}
ORD-->>User: 201 {order_id, status: CONFIRMED, eta: "2-3 days"}
KF-->>NOTIF: Send confirmation email
KF-->>FULFILL: Notify warehouse to pick and pack
Saga Compensation (Failure Handling)
flowchart TD
S1[Step 1\nReserve Inventory] -->|success| S2[Step 2\nProcess Payment]
S2 -->|success| S3[Step 3\nCreate Order Record]
S3 -->|success| S4[Step 4\nConfirm + Publish]
S1 -->|out of stock| E1[Return 409\nNo cleanup needed]
S2 -->|payment failed| C1[Compensation:\nRelease inventory reservation]
S3 -->|DB write failed| C2[Compensation:\nRefund payment\nRelease inventory]
S4 -->|Kafka down| LOG[Log event for retry\nOrder is still valid]
| Step | Action | Compensation if Later Step Fails |
|---|---|---|
| 1 | Reserve inventory | Release reservation |
| 2 | Process payment | Refund payment |
| 3 | Create order record | Mark order as failed (idempotency prevents duplicate charges) |
| 4 | Publish events | Retry — order is already confirmed; events are non-critical |
Order State Machine
stateDiagram-v2
[*] --> PENDING : POST /orders received
PENDING --> CONFIRMED : Payment + inventory succeed
PENDING --> CANCELLED : Payment failed or out of stock
CONFIRMED --> PROCESSING : Warehouse picks items
CONFIRMED --> CANCELLED : User cancels before processing
PROCESSING --> SHIPPED : Package dispatched
SHIPPED --> DELIVERED : Package received
DELIVERED --> RETURNED : Customer initiates return
RETURNED --> [*]
CANCELLED --> [*]
Idempotency — Preventing Double Charges
Every checkout request carries a client-generated idempotency key:
POST /orders
Idempotency-Key: a3f7b2d1-9c4e-4a8b-b3d2-7f1c2e3a4b5c
{
"cart_id": "cart_user123",
"shipping_address_id": "addr_456",
"payment_method_id": "pm_789"
}
Server logic:
- Check if
idempotency_keyalready exists in the database - If yes → return the cached response from the first request (no duplicate processing)
- If no → process the order and store the result with the key
A network timeout may cause the client to retry, but with the same idempotency key, the server returns the first response — the customer is never double-charged.
Step 7 — Database Design
Database Selection
| Data | Database | Reason |
|---|---|---|
| Product catalog | DynamoDB | Flexible schema per category; high read throughput |
| Product search index | Elasticsearch | Full-text + filters + ranking; millisecond queries |
| Shopping carts | Redis | Sub-ms ops; session-scoped; small data per key |
| Inventory | PostgreSQL | ACID row-level locking; strong consistency required |
| Orders + payments | PostgreSQL | ACID transactions; financial data; predictable schema |
| Product metadata cache | Redis | 90%+ cache hit rate; reduces DynamoDB reads |
Product Table (DynamoDB)
{
"product_id": "prod_abc123",
"seller_id": "seller_456",
"category_id": "electronics/headphones",
"title": "Sony WH-1000XM5",
"price": 349.99,
"currency": "USD",
"avg_rating": 4.7,
"review_count": 12450,
"images": ["s3://bucket/prod_abc/img1.jpg", "..."],
"attributes": {
"brand": "Sony",
"connectivity": "Bluetooth 5.2",
"battery_hours": 30,
"noise_cancelling": true,
"weight_grams": 250
},
"status": "ACTIVE",
"created_at": "2024-01-15T10:00:00Z"
}
Access patterns:
GetItembyproduct_id— primary key lookup (O(1))- List by
category_id— GSI on category_id + created_at - List by
seller_id— GSI on seller_id
Inventory Table (PostgreSQL)
Table: inventory
product_id VARCHAR(50) PK (composite)
warehouse_id VARCHAR(50) PK (composite)
available_qty INTEGER NOT NULL CHECK (available_qty >= 0)
reserved_qty INTEGER NOT NULL DEFAULT 0
version INTEGER NOT NULL DEFAULT 0 ← optimistic locking
updated_at TIMESTAMP
PRIMARY KEY (product_id, warehouse_id)
Optimistic locking on update:
-- Reserve 2 units — only succeeds if version matches (no concurrent modification)
UPDATE inventory
SET available_qty = available_qty - 2,
reserved_qty = reserved_qty + 2,
version = version + 1
WHERE product_id = 'prod_abc123'
AND warehouse_id = 'wh_east'
AND available_qty >= 2
AND version = 7; -- must match current version
-- If 0 rows affected → another transaction won; retry or fail
Orders Table (PostgreSQL)
Table: orders
order_id VARCHAR(50) PRIMARY KEY
user_id VARCHAR(50) NOT NULL FK → users
status VARCHAR(20) PENDING | CONFIRMED | PROCESSING | SHIPPED | DELIVERED | CANCELLED
subtotal DECIMAL(10,2)
tax DECIMAL(10,2)
shipping_fee DECIMAL(10,2)
total DECIMAL(10,2)
shipping_address_id VARCHAR(50)
payment_id VARCHAR(50) (Stripe payment intent ID)
idempotency_key VARCHAR(100) UNIQUE ← duplicate prevention
created_at TIMESTAMP
updated_at TIMESTAMP
Order Items Table (PostgreSQL)
Table: order_items
order_item_id VARCHAR(50) PRIMARY KEY
order_id VARCHAR(50) FK → orders
product_id VARCHAR(50)
product_name VARCHAR(255) ← denormalized: price/name at purchase time
quantity INTEGER
unit_price DECIMAL(10,2) ← price at purchase time, not current price
subtotal DECIMAL(10,2)
INDEX: order_id (for fetching all items of an order)
product_name and unit_price are deliberately denormalized — order history must show what the customer paid, even if the product is later renamed, repriced, or deleted.
Step 8 — Caching Strategy
Redis Cache Map
| Cache | Key Pattern | TTL | Contents |
|---|---|---|---|
| Product detail | product:{product_id} |
1 hr | Full product document |
| Search results | search:{query_hash} |
5 min | Top 50 results for this query + filters |
| Inventory (read) | inv:avail:{product_id} |
30 sec | Available quantity (approximate) |
| User cart | cart:{user_id} |
30 days | Hash of product_id → qty, price |
| Guest cart | cart:guest:{session_id} |
48 hrs | Same structure as user cart |
| Flash sale token | sale:{sale_id}:tokens |
Sale TTL | Redis Set of purchase tokens |
| Session / auth | session:{user_id} |
30 days | JWT + device info |
Read-Through Cache for Products
Request arrives for product_id:
1. Check Redis → HIT? Return in < 1ms
2. MISS → Query DynamoDB (10–20ms)
3. Write result to Redis with 1-hour TTL
4. Return to client
Cache invalidation:
When seller updates product → Kafka event → invalidate Redis key
Next request repopulates the cache
With a 90%+ cache hit rate, DynamoDB only sees ~2,300 QPS instead of 23,000 QPS — a 10× reduction in database load.
Step 9 — Inventory Management and Preventing Overselling
Four approaches, from simplest to most scalable:
Approach 1 — Pessimistic Locking
-- Lock the row before reading + updating
BEGIN;
SELECT available_qty FROM inventory
WHERE product_id = 'prod_abc'
FOR UPDATE; -- acquires exclusive row lock
UPDATE inventory
SET available_qty = available_qty - 1
WHERE product_id = 'prod_abc'
AND available_qty >= 1;
COMMIT;
✅ Guarantees no overselling ❌ Creates contention under load — all concurrent requests queue on the same lock
Approach 2 — Optimistic Locking (Default Choice)
UPDATE inventory
SET available_qty = available_qty - 1,
version = version + 1
WHERE product_id = 'prod_abc'
AND available_qty >= 1
AND version = :current_version;
-- If 0 rows affected → another transaction won → retry
✅ No blocking; high throughput when conflicts are rare ❌ High retry rates during flash sales when all users hit the same row
Approach 3 — Reservation Pattern with TTL (Best for Checkout)
-- At checkout start: move qty from available to reserved (10-min TTL)
UPDATE inventory
SET available_qty = available_qty - :qty,
reserved_qty = reserved_qty + :qty
WHERE product_id = :product_id
AND available_qty >= :qty;
INSERT INTO reservations (reservation_id, product_id, qty, expires_at)
VALUES (:uuid, :product_id, :qty, NOW() + INTERVAL '10 minutes');
-- Background job: release expired reservations every 60 seconds
UPDATE inventory i
SET available_qty = i.available_qty + r.qty,
reserved_qty = i.reserved_qty - r.qty
FROM reservations r
WHERE r.product_id = i.product_id
AND r.expires_at < NOW();
DELETE FROM reservations WHERE expires_at < NOW();
✅ Fair (first-come, first-served); users know item is held for them ❌ Requires background cleanup job; TTL tuning matters
Approach 4 — Redis Atomic Counter (Flash Sales)
# Pre-load inventory into Redis before flash sale starts
SET inventory:flash:prod_abc 1000
# Each purchase attempt — atomic decrement
DECR inventory:flash:prod_abc
# Returns new count. If < 0 → sold out; rollback the decrement
# INCR inventory:flash:prod_abc (undo)
✅ Sub-millisecond, handles 100K+ ops/sec on a single Redis instance, atomic — no race conditions ❌ Redis is not durable by default; requires sync back to PostgreSQL; only practical for pre-planned hot items
Which Approach to Use?
| Scenario | Approach |
|---|---|
| Normal shopping (most products) | Optimistic locking with version |
| Limited stock items | Reservation pattern with TTL |
| Pre-announced flash sale (known inventory) | Redis atomic counter |
| Mixed traffic | Optimistic by default; Redis for flagged "hot" items |
Step 10 — Flash Sale Handling
Flash sales are the ultimate stress test — traffic can spike from 23,000 QPS to 230,000 QPS in seconds, all concentrated on a handful of hot items.
Multi-Layered Defense
flowchart TD
USERS[Burst of Users\n230K req/sec] --> CDN[Layer 1: CDN\nCache product pages\n30s TTL for stock status]
CDN --> RL[Layer 2: Rate Limiting\n10 req/sec per user\n5 req/sec per IP]
RL --> QUEUE[Layer 3: Request Queue\nKafka — buffer checkout requests]
QUEUE --> WORKERS[Layer 4: Order Workers\nProcess at controlled rate]
WORKERS --> REDIS[Layer 5: Redis Token Check\nPre-generated purchase tokens]
REDIS -->|token available| ORDER[Create Order\nPostgreSQL]
REDIS -->|no token| SOLD[Return 410 Sold Out]
Strategy 1 — Request Queuing
Enable "queue mode" for hot products when a flash sale starts:
1. Incoming purchase requests → Kafka topic (not directly to Order Service)
2. Worker pool consumes from queue at a controlled rate (e.g., 500 orders/sec)
3. Users see real-time position: "You are #1,234 in line. Est. wait: 3 min."
4. Workers process orders → PostgreSQL sees only 500 writes/sec, not 230K
The database sees only successful, controlled traffic — not the raw burst. First users to click are first served.
Strategy 2 — Pre-Computed Inventory Tokens
For planned sales with known inventory (e.g., 1,000 units of a limited edition):
# Before the sale: pre-generate exactly 1,000 tokens
SADD sale:flash123:tokens token_1 token_2 ... token_1000
# At checkout: atomically pop a token
SPOP sale:flash123:tokens
# Returns token → proceed to payment
# Returns nil → sold out (return 410)
Physically cannot oversell — there are exactly as many tokens as items. O(1), handles any concurrency.
Strategy 3 — CDN + Static Caching
- Cache the product page at CDN edge with a 30-second TTL for stock status
- Show approximate counts: "Almost sold out!" instead of "3 remaining" for 29 of every 30 seconds
- Absorbs 99%+ of page view traffic at the edge — never reaches the origin
Strategy 4 — Graceful Degradation
When the system is under extreme load, shed non-essential work:
| Feature | Under Normal Load | Under Flash Sale Load |
|---|---|---|
| Personalized recs | Fully computed | Show static "popular items" |
| Search results | Fresh from ES | Serve cached (potentially stale) |
| Product reviews | Full, paginated | Summarized count + avg rating only |
| Analytics events | Real-time | Batched, delayed |
| Recommendation refresh | Per-session | Hourly batch only |
The core purchase flow (search → add to cart → checkout) stays fully functional and responsive.
Step 11 — Recommendation System
Amazon attributes 35% of its revenue to "Customers who bought this also bought..." widgets.
Pipeline Architecture
flowchart TD
EVENTS[User Events\nviews, purchases, searches] --> KF[Kafka]
KF --> EP[Event Processor]
EP --> FS[(Feature Store\nUser + Item vectors)]
FS --> ML[ML Training\nCollaborative + Content filtering]
ML --> MS[(Model Store\nEmbeddings)]
REC_REQ[Product Page\nRequest] --> RS[Recommendation Service]
RS --> REDIS2[(Redis Cache\nPre-computed recs)]
REDIS2 -->|cache hit| RESULT[Return recommendations]
RS -->|cache miss| MS
MS --> RERANK[Re-rank by\nuser session context]
RERANK --> REDIS2
RERANK --> RESULT
style KF fill:#ede9fe,stroke:#7c3aed
style REDIS2 fill:#fff4e0,stroke:#f59e0b
Recommendation Types
| Widget | Signal | Where |
|---|---|---|
| Frequently bought together | Co-purchase graph (items in same order) | Product page |
| Customers also viewed | Co-view graph (same session) | Product page |
| Based on your history | User purchase + view embeddings | Homepage |
| Trending in category | Sales count last 24 hours | Category page |
| Recently viewed | User's own session history | Sidebar |
Algorithm Approaches
| Approach | Mechanism | Weakness |
|---|---|---|
| Collaborative filtering | "Users like you also bought Y" | Cold start for new items/users |
| Content-based filtering | "Similar attributes to items you liked" | Filter bubble effect |
| Hybrid (production) | Deep learning embeddings (user + item) | Needs large training data |
Performance: Recommendations appear on high-traffic pages — cache them aggressively. Pre-compute for popular products. Load asynchronously (AJAX after page render). Graceful fallback to trending items if personalization times out.
Step 12 — Scaling
Read Path Scaling
flowchart LR
23K_QPS[23,000 QPS reads] --> CDN[CDN\n~70% hit rate on images]
CDN --> AG[API Gateway]
AG --> REDIS[Redis Cache\n~90% hit rate on products]
REDIS -->|10% miss| DYNAMO[DynamoDB\n~2,300 QPS]
DYNAMO --> REPLICA[DynamoDB\nRead Replicas\nmulti-region]
- CDN absorbs all image requests (~70% of total bytes)
- Redis absorbs 90% of product metadata requests
- DynamoDB only sees ~2,300 QPS — well within its capacity
Write Path Scaling
- Order Service is stateless — scale horizontally with pods
- PostgreSQL orders table: range partition by
created_at— current month = hot partition; older months archived - Read replicas for order history queries (GET /orders/{id})
- Kafka partitioned by
user_id— all events for one user processed in order
Flash Sale Auto-Scaling
Pre-flash sale preparation (T-30 min):
1. Increase CDN cache TTL on product pages: 5 min → 60 sec
2. Pre-load product inventory into Redis counters
3. Increase API Gateway rate limits slightly (authenticated users: 20 req/sec)
4. Spin up 2x Order Service pods pre-emptively
5. Enable "queue mode" for the specific product_id
Post-flash sale (T+2 hours):
1. Auto-scale down pods based on traffic metrics
2. Flush Redis tokens; sync final inventory count to PostgreSQL
3. Resume normal CDN TTLs
Step 13 — Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| Redis cache down | Cache miss storm hits DynamoDB | Redis Sentinel failover; circuit breaker caps DynamoDB blast radius |
| Elasticsearch down | Search returns 503 | Fallback to DynamoDB-based category browse; show "Search unavailable" |
| Payment Service timeout | Checkout stalls | Idempotency key + retry; client retries get cached response |
| Order DB primary down | Checkout writes fail | Automatic PostgreSQL failover < 60s; orders queued in Kafka |
| Kafka down | Order events not published | Order is still confirmed; events in dead letter queue; retry on recovery |
| Inventory over-decrement | Negative stock | CHECK (available_qty >= 0) constraint; floor at zero |
| Flash sale token exhaustion | Sold out | SPOP returns nil → instant 410 response; never reaches order system |
| DynamoDB throttling | Product detail pages slow | Exponential backoff + Redis cache; most reads hit Redis anyway |
| Fulfillment Service down | Orders not picked | Kafka retains events; Fulfillment picks up on recovery |
Final Architecture
flowchart TD
CLIENTS[Web + Mobile Clients] --> CDN[CDN\nImages + Static Assets]
CLIENTS --> LB[Load Balancer]
LB --> AG[API Gateway\nAuth + Rate Limiting]
AG --> SRCH[Search Service]
AG --> PROD[Product Service]
AG --> CART[Cart Service]
AG --> ORD[Order Service]
AG --> INV[Inventory Service]
ORD --> PAY[Payment Service]
ORD --> KF[Kafka Cluster]
INV --> KF
KF --> NOTIF[Notification Service]
KF --> FULFILL[Fulfillment Service]
KF --> IDX[Search Indexer]
SRCH --> ES[(Elasticsearch)]
IDX --> ES
PROD --> REDIS[(Redis Cluster\nProduct + Cart Cache)]
CART --> REDIS
PROD --> DDB[(DynamoDB\nProduct Catalog)]
INV --> PG[(PostgreSQL\nInventory + Orders)]
ORD --> PG
PAY --> STRIPE[Stripe / PayPal]
style CDN fill:#fef3c7,stroke:#d97706
style KF fill:#ede9fe,stroke:#7c3aed
style REDIS fill:#fff4e0,stroke:#f59e0b
style ES fill:#e8f0fe,stroke:#3b82d4
style PG fill:#f0fdf4,stroke:#16a34a
Technology Stack
| Layer | Technology |
|---|---|
| CDN | CloudFront / Akamai |
| Load Balancer | AWS ALB / NGINX |
| API Gateway | AWS API Gateway / Envoy + JWT auth |
| Product catalog | DynamoDB (flexible schema, high read throughput) |
| Product search | Elasticsearch (full-text + filters + ranking) |
| Cart store | Redis Cluster (hash per user) |
| Inventory + Orders | PostgreSQL (ACID, row-level locking) |
| Product image storage | S3 + CloudFront |
| Caching layer | Redis Cluster (product metadata, search results) |
| Event streaming | Apache Kafka (RF=3) |
| CDC (search sync) | Debezium → Kafka → Indexer Service |
| Payment | Stripe / PayPal gateway |
| Recommendation ML | Apache Spark + TensorFlow + FAISS |
| Monitoring | Prometheus + Grafana + Jaeger |
| Deployment | Kubernetes multi-region (AWS) |
Key Trade-Offs
| Decision | Option A | Option B | Choice and Reason |
|---|---|---|---|
| Product catalog DB | PostgreSQL | DynamoDB | DynamoDB — flexible per-category schema; 23K reads/sec; horizontal scale |
| Inventory locking | Pessimistic | Optimistic + Redis for hot items | Optimistic for normal; Redis counter for flash sales — best of both worlds |
| Cart reservation | Reserve at cart-add | Check only, reserve at checkout | Check only — 70% cart abandonment makes early reservation wasteful |
| Flash sale buffering | Direct to Order Service | Kafka request queue | Kafka queue — protects Order Service and PostgreSQL from burst traffic |
| Search indexing | Synchronous (block writes) | Async via CDC + Kafka | Async CDC — product writes are not blocked by Elasticsearch indexing latency |
| Checkout atomicity | Distributed transaction | Saga with compensations | Saga — payment processors don't support 2PC; Saga provides eventual consistency |
| Order history | DynamoDB | PostgreSQL | PostgreSQL — orders need ACID, joins (items), and strong consistency |
Common Interview Mistakes
- ❌ Using PostgreSQL for the product catalog — 100M products with varied attributes needs flexible schema (DynamoDB/MongoDB)
- ❌ Reserving inventory at cart-add time — 70% cart abandonment causes most items to appear falsely "out of stock"
- ❌ Synchronous payment blocking checkout — use async Kafka flow; idempotency keys prevent double charges on retry
- ❌ No Saga pattern for checkout — distributed transactions across DB + payment processor are not possible
- ❌ No inventory reservation TTL — abandoned checkouts permanently block stock without an expiry mechanism
- ❌ Single database for everything — inventory needs ACID PostgreSQL; products need flexible DynamoDB; caching needs Redis
- ❌ No flash sale strategy — 10x spike with request concentration requires queuing, Redis tokens, and rate limiting
- ❌ Not mentioning CDN — product images (250 TB) must be served from CDN edges; origin delivery is infeasible
- ❌ No idempotency keys — network retries cause double charges without them
- ❌ Real-time recommendations — pre-compute and cache for popular products; real-time ML per request is too slow
Interview Questions
- How do you prevent overselling when 10,000 users all try to buy the last 100 units simultaneously?
- What is the Saga pattern and why is it used for checkout instead of a distributed transaction?
- Why use DynamoDB for the product catalog instead of PostgreSQL?
- How does the inventory reservation TTL work, and what happens when it expires?
- How do you handle a network timeout during checkout that causes the client to retry?
- How does Elasticsearch stay in sync with the product database? What is CDC?
- Walk me through the complete flow when a user clicks "Place Order."
- Why not reserve inventory when the user adds an item to their cart?
- How would you design the system to handle a flash sale with 230,000 QPS?
- What is the pre-computed token approach for flash sales and what are its trade-offs?
- How does search ranking work? What signals does it use beyond text relevance?
- How do you ensure the cart is consistent across devices for logged-in users?
- What happens if payment succeeds but the Order DB write fails?
- Why is
unit_pricestored in theorder_itemstable instead of looking it up from the product? - How would you scale the Order Service to handle 10x traffic during Prime Day?
Summary
| Concern | Solution |
|---|---|
| Product catalog | DynamoDB — flexible schema per category; high read throughput |
| Product search | Elasticsearch with BM25 + popularity + personalization scoring |
| Search freshness | CDC (Debezium) → Kafka → Indexer → Elasticsearch (1–5s lag) |
| CDN strategy | 30-day TTL for images; 5-min TTL for search results |
| Shopping cart | Redis hash per user — sub-ms ops; async persist for cross-device sync |
| Inventory locking | Optimistic locking (normal); Redis atomic counter (flash sales) |
| Inventory reservation | Reserve at checkout with 15-min TTL; background job releases expired holds |
| Checkout reliability | Saga pattern — each step has a compensating action on failure |
| Double charge prevention | Idempotency keys — same key returns cached result, never reprocesses |
| Flash sale scaling | CDN caching + rate limiting + Kafka queue + Redis tokens + graceful degradation |
| Order history | PostgreSQL with ACID; denormalized price + name at purchase time |
| Recommendations | Offline batch (Spark/TF) + online re-rank; cached in Redis per product |
| Scale | CDN + Redis cache 90%+ of reads; stateless services; Kafka async processing |
The core principle: Amazon is a read-heavy platform with an extreme write-safety requirement. Cache aggressively for reads (CDN + Redis absorb 90%+ of traffic). For writes, the inventory → payment → order sequence must be bulletproof — the Saga pattern with compensations and idempotency keys is the correct answer. Flash sales require a completely separate strategy: queue, token, and degrade.