Imagine opening Amazon or Flipkart during a major sale.
You search for a product, open its page, add it to your cart, choose an address, pay for the order, and eventually receive the package.
From the customer's perspective, the experience is simple:
Search
↓
Product
↓
Cart
↓
Checkout
↓
Payment
↓
Order
↓
Delivery
Behind the scenes, however, many independent systems are working together.
The platform has to search through hundreds of millions of products, serve millions of product-page requests, keep carts available across devices, maintain accurate inventory, process payments, create orders, communicate with warehouses, and survive huge traffic spikes during events such as Black Friday or Prime Day.
The most interesting part of the design is not the product catalog itself. The difficult problems appear when large traffic, concurrency, inventory, payment, and distributed services meet each other.
This article walks through the system from the beginning and gradually introduces those problems.
1. Understanding the Requirements
Let's start with the customer experience.
A customer should be able to browse the product catalog and search for products using text and filters.
For example:
"wireless headphones"
The customer might then filter by:
Brand = Sony
Price = ₹5,000–₹30,000
Rating > 4
Category = Headphones
After finding a product, the customer should be able to view its details, add it to a cart, and proceed to checkout.
During checkout, the system needs to:
Validate the cart
↓
Check the current price
↓
Check inventory
↓
Reserve inventory
↓
Calculate the final amount
↓
Authorize payment
↓
Create the order
After the order is created, other systems take over:
Order
↓
Fulfillment
↓
Shipping
↓
Delivery
The platform also needs to support sellers, inventory management, notifications, recommendations, reviews, and analytics.
For this design, the most important parts are:
- Product catalog
- Search
- Cart
- Inventory
- Checkout
- Payment
- Order management
- Fulfillment
- Notifications
The internals of warehouse management, advanced fraud detection, and recommendation algorithms can be treated as separate systems.
2. The First Important Observation
Not every part of an e-commerce platform has the same consistency requirements.
Consider a product search.
A customer searches:
"iPhone"
It is perfectly acceptable if a newly added product takes a few seconds to appear in search.
Now consider inventory.
Suppose only one phone remains:
Stock = 1
Two customers attempt to buy it at nearly the same time.
The system must not sell two phones.
This gives us an important distinction:
Browsing / Search
↓
Very high traffic
Eventual consistency is often acceptable
Checkout / Inventory / Payment
↓
Correctness is critical
Strong consistency is required
This distinction will influence almost every architectural decision we make.
3. How Much Traffic Are We Designing For?
Before choosing databases and services, we need some scale assumptions.
Suppose our platform has:
100M+ registered users
10M+ daily active users
100M–500M products
Millions of orders per day
Tens or hundreds of thousands of search requests per second
The exact numbers aren't important. They are assumptions that help us reason about the architecture.
Suppose we process:
10M orders/day
The average is only around:
10,000,000 / 86,400
≈ 116 orders/sec
That number might look manageable.
But designing for 116 requests/sec would be a mistake.
Traffic is not evenly distributed.
During a large sale, the system might suddenly receive:
10K+ checkout attempts/sec
and search traffic could be much higher.
So we design for peak traffic, not average traffic.
This immediately tells us that different workloads need different scaling strategies.
4. The High-Level Architecture
A useful first version of the architecture looks like this:
USERS
|
▼
CDN / API Gateway
|
┌────────────┼────────────┐
↓ ↓ ↓
Product Search Cart
Service Service Service
| | |
↓ ↓ ↓
Product DB Elasticsearch Redis
|
↓
Kafka
|
┌──────┼───────────────┐
↓ ↓ ↓
Inventory Order Other Events
| |
↓ ↓
Inventory Order DB
DB
|
↓
Checkout
|
┌───┼────────────┐
↓ ↓ ↓
Inventory Pricing Payment
|
▼
Payment Gateway
|
▼
Order
|
▼
Kafka
|
┌───────────┼───────────┐
↓ ↓ ↓
Notification Warehouse Analytics
This diagram looks complicated, but the reasoning is straightforward.
We separate the system according to the workload:
Product browsing → cache / CDN / search index
Search → Elasticsearch
Cart → Redis
Inventory → transactional database
Orders → transactional database
Events → Kafka
Payment → payment provider
Flash sales → Redis + queues
Let's understand why each piece exists.
5. Building the Product Catalog
The product catalog is the foundation of the platform.
A product might contain:
productId
name
description
brand
category
price
images
attributes
rating
The difficult part is that different categories have different attributes.
A T-shirt might have:
size
color
material
fit
A laptop might have:
CPU
RAM
storage
screen size
GPU
A book might have:
ISBN
author
publisher
language
Because the structure isn't identical for every product, a document-oriented database such as MongoDB or DynamoDB can be a reasonable choice for the catalog.
A simplified document could look like:
{
"productId": "P123",
"name": "Sony Headphones",
"brand": "Sony",
"category": "Electronics",
"price": 34999,
"attributes": {
"color": "Black",
"connectivity": "Bluetooth"
}
}
The catalog database is the source of truth for product information.
But we shouldn't use this database directly for every product search.
6. Why Search Needs Its Own System
Searching a large catalog is a different problem from storing products.
Suppose the customer searches:
wireless headphones
and wants:
Brand = Sony
Price = ₹5,000–₹30,000
Rating >= 4
Availability = In stock
A relational or document database can perform simple queries, but at very large scale we want a system specifically optimized for search.
This is where Elasticsearch comes in.
The architecture becomes:
Product Database
|
↓
Product Changed Event
|
↓
Kafka
|
↓
Search Indexer
|
↓
Elasticsearch
Elasticsearch can provide:
- Full-text search
- Filtering
- Faceted navigation
- Autocomplete
- Fuzzy matching
- Relevance ranking
The customer request then becomes:
User
↓
Search API
↓
Elasticsearch
↓
Search results
This keeps heavy search traffic away from the transactional product database.
7. Why Search Can Be Eventually Consistent
Suppose a seller creates a new product.
The product is immediately saved:
Product DB
↓
Product exists
Then an event is published:
Product Created
↓
Kafka
↓
Search Indexer
↓
Elasticsearch
There may be a short delay.
For a few seconds:
Product DB → new product exists
Elasticsearch → product not indexed yet
That's usually fine.
Search doesn't have to be perfectly synchronized every millisecond.
But checkout is different.
Suppose Elasticsearch says:
Price = ₹50,000
while the product database says:
Price = ₹55,000
We cannot let the customer pay based only on the search result.
Therefore, during checkout:
Search result
↓
Re-fetch authoritative data
↓
Validate current price
↓
Validate current inventory
The search index helps us find the product.
It does not become the source of truth for money or inventory.
8. Making Product Reads Fast
Product pages can receive enormous traffic.
A popular product might be viewed millions of times.
We don't want:
1M requests
↓
Product Database
Instead, we can cache frequently accessed product information in Redis.
User
↓
Product Service
↓
Redis
For example:
product:P123
name = iPhone
price = ₹80,000
brand = Apple
...
If the value exists in Redis:
Redis → return immediately
If it doesn't:
Redis miss
↓
Product DB
↓
Store in Redis
↓
Return
This is a classic cache-aside pattern.
The database remains authoritative.
Redis exists primarily to make common reads fast and protect the database from excessive read traffic.
9. Designing the Shopping Cart
Now the customer adds a product to the cart.
A cart might look like:
Cart
-------------------------
iPhone × 1
Headphones × 2
For a logged-in customer, Redis is a good fit because cart operations are frequent and latency-sensitive.
For example:
cart:user123
iPhone → 1
Headphones → 2
A TTL can eventually remove abandoned carts.
Guest users are slightly different.
A guest cart can initially live in the browser. When the user logs in, the guest cart can be merged with the user's server-side cart.
The important thing is that the cart should not be treated as the final source of truth for price or inventory.
10. The Cart Price Problem
Suppose a customer adds headphones for:
₹10,000
to the cart.
Two hours later, the seller changes the price:
₹12,000
What should happen?
The cart might still display the old price.
But when checkout begins, we must revalidate it.
The flow becomes:
Cart
↓
Fetch current product price
↓
Fetch current promotions
↓
Calculate current total
The server, not the browser, determines the final amount.
A customer should never be able to send:
{
"price": 1
}
and expect the server to charge ₹1 for a ₹10,000 product.
11. Inventory Is Where the Design Gets Interesting
Inventory is one of the hardest parts of an e-commerce system because of concurrency.
Suppose:
iPhone stock = 1
At exactly the same time:
User A → BUY
User B → BUY
Both requests could read:
stock = 1
If we simply perform:
Read stock
↓
Check stock
↓
Decrease stock
both requests could succeed.
The result would be:
Two customers
↓
Two orders
↓
One physical phone
That's overselling.
We need an atomic operation.
12. A Simple Inventory Solution: Database Locking
One approach is a database transaction with a row lock.
Conceptually:
BEGIN;
SELECT quantity
FROM inventory
WHERE product_id = ?
FOR UPDATE;
-- check quantity
UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = ?;
COMMIT;
The first transaction locks the inventory row.
If stock is:
1
the first buyer reserves it:
1 → 0
When the second buyer gets the lock:
0 → reject
This is easy to reason about and gives strong consistency.
But there is a problem.
13. Why Database Locking Isn't Enough for Flash Sales
Imagine a flash sale:
Stock = 1,000
and:
1,000,000 users
all trying to buy the same product.
Now one database row becomes extremely hot.
Thousands of requests may compete for the same lock.
We can get:
High lock contention
Slow transactions
Connection pool exhaustion
Database CPU pressure
So we need a different strategy for extreme traffic.
14. Atomic Inventory Updates
For normal high-volume traffic, we can often avoid explicit locks by making the database update itself atomic.
For example:
UPDATE inventory
SET quantity = quantity - 1
WHERE product_id = ?
AND quantity >= 1;
If:
rows affected = 1
the reservation succeeded.
If:
rows affected = 0
there was no inventory available.
The important part is that the database performs:
Check quantity
+
Decrease quantity
as one atomic operation.
This is often a better starting point than pessimistic locking.
15. Inventory Reservations
There is another important concept: we shouldn't necessarily permanently consume inventory as soon as a customer starts checkout.
Suppose:
Stock = 10
A customer starts checkout.
We reserve one item:
Available = 9
Reserved = 1
The customer now has, for example, 15 minutes to complete payment.
If payment succeeds:
Reserved → Sold
If the customer abandons checkout:
Reservation expires
↓
Reserved → Available
This prevents customers from holding inventory forever.
The reservation lifecycle can be represented as:
AVAILABLE
↓
RESERVED
↓
├── PAYMENT SUCCESS → SOLD
|
└── TIMEOUT → AVAILABLE
16. Flash Sales Require a Different Approach
Now consider a limited product:
1,000 units
and:
1,000,000 users
We cannot allow all million requests to hit the inventory database.
Instead, Redis can act as a very fast admission-control layer.
Before the sale:
flash:stock:P123 = 1000
A purchase attempts an atomic decrement:
DECR flash:stock:P123
Redis can process these operations extremely quickly.
Once the counter reaches zero:
Sold out
But Redis should not become our only permanent source of truth.
A better architecture is:
Millions of users
↓
Rate Limiter
↓
Queue
↓
Redis atomic stock counter
↓
Successful buyers
↓
Order workers
↓
Durable inventory database
Redis protects the database from the enormous burst.
The database remains the durable record.
A reconciliation process can compare Redis and the database and repair any inconsistencies.
17. Protecting the System With a Queue
Even Redis can receive a huge number of requests.
A queue gives the system backpressure.
Instead of:
1M users
↓
1M synchronous requests
↓
Database
we can do:
1M users
↓
Queue
↓
Controlled number of workers
↓
Inventory / Order services
The customer might see:
"You are in the queue."
This allows the system to process successful purchase attempts at a rate the downstream services can handle.
18. Checkout Is a Distributed Workflow
Now let's put the major pieces together.
Suppose the customer has:
iPhone ₹80,000
Headphones ₹10,000
and clicks:
Place Order
The checkout workflow might be:
Validate Cart
↓
Re-check Prices
↓
Reserve Inventory
↓
Calculate Final Amount
↓
Authorize Payment
↓
Create Order
↓
Confirm Inventory
↓
Publish Order Event
Notice that this involves multiple services:
Cart
Inventory
Pricing
Payment
Order
Each may have its own database.
That creates a distributed transaction problem.
19. Why Not Use One Giant Transaction?
We might imagine:
BEGIN
Reserve Inventory
Charge Payment
Create Order
COMMIT
But inventory, payment, and orders may be separate services.
Traditional database transactions don't naturally span all of them.
Two-Phase Commit is one possible solution, but it introduces:
Blocking
High latency
Tight coupling
Complex failure handling
For a microservice architecture, a Saga is usually more practical.
20. The Saga Pattern
A Saga breaks the checkout workflow into smaller operations.
For example:
1. Reserve Inventory
2. Authorize Payment
3. Create Order
Suppose:
Reserve Inventory → SUCCESS
Authorize Payment → SUCCESS
Create Order → FAILURE
We need compensating actions.
Create Order → FAILED
↓
Void Payment Authorization
↓
Release Inventory
The important idea is:
A distributed transaction is replaced by a sequence of local transactions plus compensating actions.
21. Orchestrating the Checkout
There are two common ways to implement a Saga.
With choreography, services communicate through events:
Inventory Reserved
↓
Payment Service
↓
Payment Completed
↓
Order Service
This is loosely coupled, but large workflows can become difficult to understand.
With orchestration, one service coordinates the workflow:
Order Orchestrator
|
┌────────────┼────────────┐
↓ ↓ ↓
Inventory Payment Order
The orchestrator knows:
What step comes next?
What happens when a step fails?
What compensation is required?
For checkout, orchestration is often easier to reason about and debug.
22. Payment Is Another Reliability Problem
Now suppose the payment service calls an external payment provider.
The request looks like:
Our Payment Service
↓
Payment Gateway
↓
Bank / Card Network
Imagine the bank successfully charges:
₹10,000
but our connection times out before we receive the response.
Our service doesn't know whether the payment succeeded.
If we simply retry:
Retry
↓
Another ₹10,000 charge
the customer could be charged twice.
This is why payment APIs must be idempotent.
23. Idempotency
Every logical payment attempt gets an idempotency key:
PAYMENT-123
The first request:
PAYMENT-123
↓
SUCCESS
A retry with the same key:
PAYMENT-123
↓
Already processed
↓
Return previous result
The payment provider and our own payment service should both support this concept.
The payment table can store:
paymentId
orderId
amount
status
provider
idempotencyKey
createdAt
and enforce uniqueness on the idempotency key.
24. Authorize First, Capture Later
A useful payment flow is:
Order placement
↓
Payment authorization
↓
Order processing
↓
Product shipped
↓
Payment capture
Authorization means the payment method has enough funds and the amount is reserved.
If the order is cancelled before shipment:
Cancel order
↓
Void authorization
instead of charging the customer and then issuing a refund.
This is particularly useful for physical goods because payment capture can be tied to fulfillment.
25. The Order State Machine
An order should have explicit states.
For example:
PLACED
↓
PAYMENT_AUTHORIZED
↓
CONFIRMED
↓
PROCESSING
↓
SHIPPED
↓
DELIVERED
Other possible states include:
CANCELLED
RETURNED
REFUNDED
The state machine becomes very useful when failures happen.
For example:
Payment = AUTHORIZED
Order = PAYMENT_AUTHORIZED
Inventory = RESERVED
If the order service crashes, we know where the workflow stopped.
We can resume or compensate from that state.
26. Keeping Services in Sync With Kafka
Once an order is successfully created, many other systems need to know about it.
For example:
Notification
Warehouse
Analytics
Recommendations
Seller dashboard
Fraud detection
We don't want checkout to wait for all of them.
Instead:
Create Order
↓
Publish OrderCreated
↓
Return response
Consumers process the event asynchronously:
OrderCreated
|
┌──────────────┼──────────────┐
↓ ↓ ↓
Notification Warehouse Analytics
Kafka is useful as the event backbone.
Typical event streams might include:
product-events
inventory-events
order-events
user-events
click-events
This keeps downstream systems decoupled from the checkout path.
27. What If Kafka Is Down?
Suppose:
Order saved successfully
↓
Kafka unavailable
We cannot afford to lose the OrderCreated event.
A common solution is the transactional outbox pattern.
Inside the same database transaction:
BEGIN
Save Order
Save Outbox Event
COMMIT
Now both the order and the event are durable.
A background worker later publishes:
Outbox
↓
Kafka
If Kafka is temporarily unavailable, the worker retries.
This creates a reliable bridge between the transactional database and the asynchronous event system.
28. Making Kafka Processing Reliable
Distributed messaging usually works with at-least-once delivery.
That means a consumer may receive the same event more than once.
For example:
OrderCreated
↓
Consumer processes event
↓
Consumer crashes before acknowledging
↓
Kafka delivers event again
Therefore consumers should be idempotent.
A common pattern is to give each event a unique:
eventId
and record processed event IDs when necessary.
We also need to monitor:
Kafka consumer lag
If consumers cannot keep up with producers, lag grows.
Messages that repeatedly fail can be moved to a dead-letter queue for investigation.
29. API Design
Once the core architecture is clear, the major APIs become straightforward.
Search:
GET /api/v1/products/search?q=wireless+headphones&category=electronics&minPrice=5000&maxPrice=30000
Product:
GET /api/v1/products/{productId}
Add to cart:
POST /api/v1/carts/{cartId}/items
Get cart:
GET /api/v1/carts/{cartId}
Create order:
POST /api/v1/orders
Idempotency-Key: checkout-123
Get order:
GET /api/v1/orders/{orderId}
Payment:
POST /api/v1/payments
Idempotency-Key: payment-123
The important API design principle is that operations which can be retried should be made idempotent.
30. Data Model
The catalog can use a document model because product attributes vary.
For example:
Product
-------------------------
productId
sellerId
name
category
price
attributes
images
rating
createdAt
updatedAt
Inventory is more transactional:
Inventory
-------------------------
productId
warehouseId
availableQuantity
reservedQuantity
updatedAt
Primary Key:
(productId, warehouseId)
The cart can contain:
Cart
-------------------------
cartId
userId
status
createdAt
updatedAt
expiresAt
and:
CartItem
-------------------------
cartId
productId
quantity
priceAtAddTime
Orders should preserve the actual price agreed during checkout:
Order
-------------------------
orderId
userId
status
subtotal
totalAmount
currency
createdAt
updatedAt
Order items:
OrderItem
-------------------------
orderId
productId
sellerId
quantity
unitPrice
Payment:
Payment
-------------------------
paymentId
orderId
amount
currency
status
provider
idempotencyKey
createdAt
updatedAt
An order should never recalculate its historical price from the current product price.
31. Choosing the Databases
There is no requirement to use one database for everything.
A reasonable large-scale architecture might use:
Catalog
→ MongoDB / DynamoDB
Search
→ Elasticsearch
Cart
→ Redis
Orders
→ PostgreSQL / MySQL
Inventory
→ Strongly consistent relational DB
Events
→ Kafka
The important principle is:
Choose storage according to the access pattern and consistency requirement.
For a smaller system, PostgreSQL could handle much more of the platform.
The more specialized architecture becomes useful as traffic and scale increase.
32. Partitioning and Sharding
Eventually, an order database may become too large for one machine or one database cluster.
Orders can be partitioned using a key such as:
userId
if the common query is:
Show all orders for this user
For example:
Shard 1 → users A–F
Shard 2 → users G–M
Shard 3 → users N–S
Shard 4 → users T–Z
The exact partitioning strategy depends on the workload.
We must also watch for hot partitions.
A very large seller or customer can generate disproportionately high traffic.
33. Multi-Warehouse Inventory
Large marketplaces rarely have one warehouse.
Imagine:
Bangalore → 5 iPhones
Mumbai → 10 iPhones
Delhi → 2 iPhones
Inventory therefore becomes:
Product
|
├── Bangalore → 5
├── Mumbai → 10
└── Delhi → 2
When a customer places an order, the system needs to decide where the order should be fulfilled.
Possible factors include:
Availability
Customer location
Shipping cost
Delivery time
Warehouse capacity
Ideally, we ship from a nearby warehouse that has inventory.
34. Multi-Seller Orders
A marketplace can have products from several sellers in one cart:
iPhone → Seller A
Shoes → Seller B
Book → Seller C
The customer experiences one checkout, but internally the platform may create separate fulfillment orders:
Customer Order
|
├── Seller A Order
├── Seller B Order
└── Seller C Order
Each seller may ship independently.
Payment may also need to be split into:
Customer payment
↓
Platform commission
↓
Seller payouts
This is one reason marketplace systems are considerably more complicated than a simple online store.
35. Pricing and Promotions
The final checkout price may involve several steps:
Base price
↓
Discount
↓
Coupon
↓
Tax
↓
Shipping
↓
Final amount
A promotion might have:
Expiry
Usage limit
Eligible products
Eligible users
Minimum order value
The pricing service must validate all of these.
The client must never be trusted to supply the final price.
36. Recommendations and Reviews
Recommendations don't need to block checkout.
A recommendation system might use:
Purchase history
Browsing history
Product similarity
Co-purchase patterns
For example:
Customers who bought A
also bought B
The recommendation results can be precomputed and stored in Redis.
Reviews and ratings can also be handled asynchronously.
A new review taking a few seconds to appear in an aggregate rating is generally acceptable.
37. Rate Limiting
Rate limiting protects the system from abuse and sudden bursts.
For example:
Search
→ relatively high limit
Add to Cart
→ lower limit
Checkout
→ much lower limit
A distributed rate limiter can use Redis counters.
The flow is:
Client
↓
API Gateway
↓
Rate Limiter
↓
Application Service
During a flash sale, rate limiting becomes especially important because the goal is not merely to accept traffic but to protect downstream services from being overwhelmed.
38. CDN
Product images and static assets are excellent CDN candidates.
Instead of:
User
↓
Application server
↓
Object storage
we can use:
User
↓
CDN
↓
Cache hit
For a cache miss:
CDN
↓
Origin
↓
Cache
↓
User
This keeps huge amounts of image and static-content traffic away from application servers.
39. Observability
At this scale, knowing that a service is "up" isn't enough.
We need to know whether the entire customer journey is healthy.
Useful metrics include:
Search p95 / p99 latency
Product API latency
Checkout latency
Checkout success rate
Inventory reservation failures
Payment failure rate
Order creation failures
Redis hit ratio
Database latency
Database connection usage
Kafka consumer lag
Flash-sale queue depth
Distributed tracing can follow:
Request
↓
API Gateway
↓
Order Service
↓
Inventory Service
↓
Payment Service
↓
Database
A correlation ID makes it possible to trace one checkout across multiple services.
40. Failure Handling
Distributed systems fail.
The architecture should assume this rather than treating failure as an exceptional event.
If the inventory service is unavailable:
Checkout
↓
Inventory unavailable
↓
Do not create a confirmed order
↓
Retry or fail gracefully
If the payment gateway is unavailable:
Payment unavailable
↓
Do not mark payment as successful
If Kafka is unavailable:
Persist event in outbox
↓
Publish later
If Redis is unavailable:
Use the durable database where safe
However, for flash-sale admission, Redis may be part of the high-throughput control path, so the system should have an explicit degraded mode rather than blindly bypassing it.
41. Inventory Reconciliation
Because systems such as Redis, Kafka consumers, and databases can fail independently, inventory can temporarily drift.
For example:
Redis says:
10 available
Database says:
8 available
A reconciliation process can periodically compare authoritative inventory with derived state.
Inventory DB
↓
Compare
↓
Redis
↓
Repair mismatch
This is especially important after flash sales.
The database remains the durable source of truth.
42. CQRS and Read Models
At very large scale, read traffic can be separated from write traffic.
The write path is:
Command
↓
Transactional DB
Events are then published:
Transactional DB
↓
Kafka
↓
Read model builders
Those consumers update:
Elasticsearch
Redis
Other read models
Conceptually:
Commands
|
▼
Write Model
|
▼
PostgreSQL
|
Kafka
|
┌────────┴────────┐
↓ ↓
Elasticsearch Redis
Read Model Read Model
This is essentially CQRS.
The trade-off is eventual consistency between the write model and read models.
That is acceptable for search and browsing, but not for the final inventory or payment decision.
43. Security
E-commerce systems handle sensitive customer and payment information.
Important controls include:
Authentication
Authorization
TLS
Rate limiting
Input validation
Audit logging
Fraud detection
Payment card information should normally be handled by a PCI-compliant payment provider rather than stored directly in the application database.
The server must also validate:
Price
Quantity
Seller
Product
Order ownership
Coupon
A client should never be able to manipulate these values simply by changing the request.
44. Availability and Disaster Recovery
A production platform should have multiple instances of critical services.
For example:
Load Balancer
|
┌───┼───┐
↓ ↓ ↓
S1 S2 S3
Databases should use:
Replication
Backups
Automated failover
Kafka should use replication.
Redis should have appropriate replication and recovery depending on how it is being used.
For disaster recovery, backups should be tested rather than merely configured.
A recovery plan should define:
RPO
How much data can we lose?
RTO
How quickly must we recover?
45. Multi-Region Architecture
As the platform becomes global, one region may no longer be enough.
We might have:
India
Europe
US
with regional services:
India Region
↓
India Inventory
India Orders
Europe Region
↓
Europe Inventory
Europe Orders
US Region
↓
US Inventory
US Orders
Inventory is particularly difficult because it is mutable state.
A product can exist in multiple warehouses:
India → 100
Europe → 50
US → 200
We should avoid making every inventory update depend on one globally shared database if possible.
Instead, inventory can be partitioned by fulfillment region or warehouse.
Catalog and search data can be replicated more freely because those workloads tolerate eventual consistency.
46. Incident: Elasticsearch Goes Down
Suppose Elasticsearch becomes unhealthy.
Search requests start failing or becoming slow.
The important thing is that checkout should still work.
A customer who already knows the product ID should still be able to open the product page through:
Product DB / Redis
The system can:
Route to healthy search replicas
↓
Reduce non-critical search features
↓
Recover Elasticsearch
Search degradation should not bring down the entire commerce platform.
47. Incident: Inventory Reservation Leak
Suppose customers complain:
"The product says sold out, but nobody can buy it."
One possible cause is that reservations were never released.
For example:
Customer starts checkout
↓
Inventory reserved
↓
Payment fails
↓
Reservation never released
Over time:
Available inventory → artificially decreases
A reservation expiry mechanism and reconciliation job prevent this.
For example:
Reservation created
↓
15-minute TTL
↓
Payment not completed
↓
Release inventory
48. Incident: Duplicate Payment
Suppose a customer sees two payment authorizations.
We investigate using:
orderId
paymentId
idempotencyKey
provider transaction ID
The system should:
Identify duplicate
↓
Void/refund duplicate
↓
Fix retry/idempotency behavior
The important lesson is that payment APIs should be designed for retries from the beginning.
49. Cost and Operational Trade-offs
Every additional distributed system has an operational cost.
Our architecture now contains:
Redis
Elasticsearch
Kafka
Multiple databases
Payment provider
CDN
Each adds:
Infrastructure cost
Monitoring
On-call responsibility
Failure modes
Operational complexity
For a small e-commerce application, this would be overengineering.
A much smaller system could start with:
Monolith
↓
PostgreSQL
↓
Redis
and introduce Elasticsearch, Kafka, and separate services only when the scale justifies them.
This is an important architectural principle:
Don't introduce distributed complexity until the workload requires it.
50. How the Architecture Evolves
A sensible system does not need to start with twenty services.
A small platform might begin as:
Monolith
|
PostgreSQL
|
Redis
The product catalog, cart, orders, and inventory can initially live in one application.
As traffic grows, search can be separated:
Application
|
├── PostgreSQL
├── Redis
└── Elasticsearch
As asynchronous workloads grow:
Application
|
└── Kafka
|
├── Notifications
├── Analytics
└── Search indexing
As checkout becomes more complex:
Inventory Service
Payment Service
Order Service
can be separated.
During flash sales:
Redis atomic counters
+
Queue
+
Dedicated workers
can protect the core transactional systems.
Finally, global expansion can introduce:
Multi-region services
Multi-warehouse inventory
Regional order processing
Disaster recovery
The architecture evolves because the problems evolve.
51. The Most Important Trade-offs
There is no universally correct choice.
PostgreSQL vs MongoDB
PostgreSQL is excellent for:
Orders
Payments
Inventory
because transactions matter.
MongoDB/DynamoDB can be attractive for:
Flexible product attributes
Database Lock vs Atomic Update
Row locks are easy to understand but can create contention.
Atomic updates usually provide better throughput for simple inventory decrements.
Redis vs Database
Redis provides:
Speed
High throughput
Atomic counters
The database provides:
Durability
Strong source of truth
For flash sales:
Redis → absorb the burst
Database → durable truth
Saga vs 2PC
2PC gives stronger transactional coordination but introduces blocking and coupling.
Saga uses:
Local transactions
+
Compensation
and fits microservice workflows better.
Synchronous vs Asynchronous
Keep critical checkout operations synchronous.
Move non-critical work to Kafka:
Email
Analytics
Recommendations
Seller notifications
Search Index vs Primary Database
Elasticsearch is excellent for search but is not the source of truth.
The primary catalog database remains authoritative.
52. The Final Architecture
After all the deeper decisions, the architecture can be summarized as:
USERS
|
▼
CDN / Gateway
|
┌────────────────┼─────────────────┐
↓ ↓ ↓
Product Search Cart
Service Service Service
| | |
↓ ↓ ↓
Product DB Elasticsearch Redis
|
↓
Kafka
|
┌─────┴──────────┐
↓ ↓
Inventory Other
Service Consumers
|
↓
Strong Inventory DB
|
↓
Checkout
|
┌────┼───────────────┐
↓ ↓ ↓
Inv Pricing Payment
|
↓
Payment Gateway
|
↓
Order
|
↓
Kafka
|
┌───────────┼───────────┐
↓ ↓ ↓
Notification Warehouse Analytics
During a flash sale, the inventory path changes:
Millions of Users
↓
Rate Limiter
↓
Queue
↓
Redis Atomic Counter
↓
Order Workers
↓
Inventory DB
The architecture therefore has two very different paths.
Read path
User
↓
CDN / Cache
↓
Search / Product DB
Optimized for:
Very high throughput
Low latency
Eventual consistency
Transaction path
User
↓
Checkout
↓
Inventory
↓
Payment
↓
Order
Optimized for:
Correctness
Strong consistency
Idempotency
Failure recovery
53. The Core Ideas to Remember
The entire design becomes much easier to remember if we reduce it to a few principles.
First, separate read-heavy workloads from write-critical workloads.
Read-heavy
→ CDN
→ Redis
→ Elasticsearch
Write-critical
→ Transactional database
Second, inventory is the main concurrency problem.
Many buyers
↓
Limited stock
↓
Exactly one valid reservation
Third, checkout is a distributed transaction.
Inventory
+
Payment
+
Order
Use:
Saga
+
Compensation
+
Idempotency
Fourth, flash sales require traffic absorption.
Rate limiter
↓
Queue
↓
Redis
↓
Workers
↓
Durable DB
Fifth, Kafka moves non-critical work away from the request path.
Order Created
↓
Kafka
↓
Notifications
Analytics
Warehouse
Recommendations
Finally:
The database should remain the source of truth for business-critical state, while caches, search indexes, queues, and event streams help the system scale.
54. How to Explain This in a System Design Interview
A clean explanation can naturally progress like this:
Start by saying:
"I'd separate the system into a read-heavy browsing path and a consistency-critical transaction path."
Then explain the read path:
Catalog
↓
Elasticsearch
↓
Redis / CDN
Then move to checkout:
Cart
↓
Inventory
↓
Payment
↓
Order
Then explain the hardest problem:
"The biggest correctness issue is preventing overselling when multiple users attempt to buy the last item concurrently."
Explain the normal solution:
Atomic database update
Then the flash-sale solution:
Redis atomic counter
+
Queue
+
Durable inventory database
Then explain checkout consistency:
Saga
+
Compensation
Then payment:
Idempotency
+
Authorization / Capture
Then asynchronous processing:
Kafka
Finally, discuss:
Failure recovery
Observability
Sharding
Multi-warehouse
Multi-region
Trade-offs
That gives you a natural conversation instead of a list of technologies.
55. Final Mental Model
If you remember only one picture, remember this:
E-COMMERCE
|
┌───────────────┼────────────────┐
↓ ↓ ↓
CATALOG SEARCH CART
| | |
Product DB Elasticsearch Redis
| | |
└───────────────┼────────────────┘
|
CHECKOUT
|
┌──────────────┼──────────────┐
↓ ↓ ↓
INVENTORY PRICING PAYMENT
| |
Strong DB Payment Gateway
| |
└──────────────┬──────────────┘
↓
ORDER
|
↓
Kafka
|
┌──────────────┼──────────────┐
↓ ↓ ↓
Notification Warehouse Analytics
And during extreme traffic:
Millions of users
↓
Rate Limiter
↓
Queue
↓
Redis
↓
Workers
↓
Transactional Systems
The key lesson is not that every e-commerce system must use MongoDB, Redis, Elasticsearch, Kafka, and microservices.
The key lesson is why each component exists:
Elasticsearch → scalable search
Redis → fast reads / hot state / atomic counters
Kafka → asynchronous event processing
Database → durable business truth
Queue → backpressure
Saga → distributed workflow
Idempotency → safe retries
CDN → offload static traffic
Once those relationships are clear, the architecture becomes much easier to design from scratch in an interview.
Top comments (0)