DEV Community

Rohit Kori
Rohit Kori

Posted on

Designing an E-Commerce System

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

The customer might then filter by:

Brand = Sony
Price = ₹5,000–₹30,000
Rating > 4
Category = Headphones
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

After the order is created, other systems take over:

Order
  ↓
Fulfillment
  ↓
Shipping
  ↓
Delivery
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The exact numbers aren't important. They are assumptions that help us reason about the architecture.

Suppose we process:

10M orders/day
Enter fullscreen mode Exit fullscreen mode

The average is only around:

10,000,000 / 86,400
≈ 116 orders/sec
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The difficult part is that different categories have different attributes.

A T-shirt might have:

size
color
material
fit
Enter fullscreen mode Exit fullscreen mode

A laptop might have:

CPU
RAM
storage
screen size
GPU
Enter fullscreen mode Exit fullscreen mode

A book might have:

ISBN
author
publisher
language
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and wants:

Brand = Sony
Price = ₹5,000–₹30,000
Rating >= 4
Availability = In stock
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then an event is published:

Product Created
    ↓
Kafka
    ↓
Search Indexer
    ↓
Elasticsearch
Enter fullscreen mode Exit fullscreen mode

There may be a short delay.

For a few seconds:

Product DB       → new product exists
Elasticsearch    → product not indexed yet
Enter fullscreen mode Exit fullscreen mode

That's usually fine.

Search doesn't have to be perfectly synchronized every millisecond.

But checkout is different.

Suppose Elasticsearch says:

Price = ₹50,000
Enter fullscreen mode Exit fullscreen mode

while the product database says:

Price = ₹55,000
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Instead, we can cache frequently accessed product information in Redis.

User
 ↓
Product Service
 ↓
Redis
Enter fullscreen mode Exit fullscreen mode

For example:

product:P123

name = iPhone
price = ₹80,000
brand = Apple
...
Enter fullscreen mode Exit fullscreen mode

If the value exists in Redis:

Redis → return immediately
Enter fullscreen mode Exit fullscreen mode

If it doesn't:

Redis miss
    ↓
Product DB
    ↓
Store in Redis
    ↓
Return
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

to the cart.

Two hours later, the seller changes the price:

₹12,000
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The server, not the browser, determines the final amount.

A customer should never be able to send:

{
  "price": 1
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

At exactly the same time:

User A → BUY
User B → BUY
Enter fullscreen mode Exit fullscreen mode

Both requests could read:

stock = 1
Enter fullscreen mode Exit fullscreen mode

If we simply perform:

Read stock
 ↓
Check stock
 ↓
Decrease stock
Enter fullscreen mode Exit fullscreen mode

both requests could succeed.

The result would be:

Two customers
      ↓
Two orders
      ↓
One physical phone
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

The first transaction locks the inventory row.

If stock is:

1
Enter fullscreen mode Exit fullscreen mode

the first buyer reserves it:

1 → 0
Enter fullscreen mode Exit fullscreen mode

When the second buyer gets the lock:

0 → reject
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and:

1,000,000 users
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

If:

rows affected = 1
Enter fullscreen mode Exit fullscreen mode

the reservation succeeded.

If:

rows affected = 0
Enter fullscreen mode Exit fullscreen mode

there was no inventory available.

The important part is that the database performs:

Check quantity
+
Decrease quantity
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A customer starts checkout.

We reserve one item:

Available = 9
Reserved = 1
Enter fullscreen mode Exit fullscreen mode

The customer now has, for example, 15 minutes to complete payment.

If payment succeeds:

Reserved → Sold
Enter fullscreen mode Exit fullscreen mode

If the customer abandons checkout:

Reservation expires
      ↓
Reserved → Available
Enter fullscreen mode Exit fullscreen mode

This prevents customers from holding inventory forever.

The reservation lifecycle can be represented as:

AVAILABLE
    ↓
RESERVED
    ↓
    ├── PAYMENT SUCCESS → SOLD
    |
    └── TIMEOUT → AVAILABLE
Enter fullscreen mode Exit fullscreen mode

16. Flash Sales Require a Different Approach

Now consider a limited product:

1,000 units
Enter fullscreen mode Exit fullscreen mode

and:

1,000,000 users
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A purchase attempts an atomic decrement:

DECR flash:stock:P123
Enter fullscreen mode Exit fullscreen mode

Redis can process these operations extremely quickly.

Once the counter reaches zero:

Sold out
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

we can do:

1M users
   ↓
Queue
   ↓
Controlled number of workers
   ↓
Inventory / Order services
Enter fullscreen mode Exit fullscreen mode

The customer might see:

"You are in the queue."
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and clicks:

Place Order
Enter fullscreen mode Exit fullscreen mode

The checkout workflow might be:

Validate Cart
      ↓
Re-check Prices
      ↓
Reserve Inventory
      ↓
Calculate Final Amount
      ↓
Authorize Payment
      ↓
Create Order
      ↓
Confirm Inventory
      ↓
Publish Order Event
Enter fullscreen mode Exit fullscreen mode

Notice that this involves multiple services:

Cart
Inventory
Pricing
Payment
Order
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Suppose:

Reserve Inventory → SUCCESS
Authorize Payment  → SUCCESS
Create Order       → FAILURE
Enter fullscreen mode Exit fullscreen mode

We need compensating actions.

Create Order → FAILED
       ↓
Void Payment Authorization
       ↓
Release Inventory
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

This is loosely coupled, but large workflows can become difficult to understand.

With orchestration, one service coordinates the workflow:

                 Order Orchestrator
                    |
       ┌────────────┼────────────┐
       ↓            ↓            ↓
  Inventory       Payment       Order
Enter fullscreen mode Exit fullscreen mode

The orchestrator knows:

What step comes next?
What happens when a step fails?
What compensation is required?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Imagine the bank successfully charges:

₹10,000
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The first request:

PAYMENT-123
     ↓
SUCCESS
Enter fullscreen mode Exit fullscreen mode

A retry with the same key:

PAYMENT-123
     ↓
Already processed
     ↓
Return previous result
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Authorization means the payment method has enough funds and the amount is reserved.

If the order is cancelled before shipment:

Cancel order
      ↓
Void authorization
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Other possible states include:

CANCELLED
RETURNED
REFUNDED
Enter fullscreen mode Exit fullscreen mode

The state machine becomes very useful when failures happen.

For example:

Payment = AUTHORIZED
Order = PAYMENT_AUTHORIZED
Inventory = RESERVED
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We don't want checkout to wait for all of them.

Instead:

Create Order
     ↓
Publish OrderCreated
     ↓
Return response
Enter fullscreen mode Exit fullscreen mode

Consumers process the event asynchronously:

                    OrderCreated
                         |
          ┌──────────────┼──────────────┐
          ↓              ↓              ↓
    Notification      Warehouse      Analytics
Enter fullscreen mode Exit fullscreen mode

Kafka is useful as the event backbone.

Typical event streams might include:

product-events
inventory-events
order-events
user-events
click-events
Enter fullscreen mode Exit fullscreen mode

This keeps downstream systems decoupled from the checkout path.


27. What If Kafka Is Down?

Suppose:

Order saved successfully
        ↓
Kafka unavailable
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Now both the order and the event are durable.

A background worker later publishes:

Outbox
   ↓
Kafka
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Therefore consumers should be idempotent.

A common pattern is to give each event a unique:

eventId
Enter fullscreen mode Exit fullscreen mode

and record processed event IDs when necessary.

We also need to monitor:

Kafka consumer lag
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Product:

GET /api/v1/products/{productId}
Enter fullscreen mode Exit fullscreen mode

Add to cart:

POST /api/v1/carts/{cartId}/items
Enter fullscreen mode Exit fullscreen mode

Get cart:

GET /api/v1/carts/{cartId}
Enter fullscreen mode Exit fullscreen mode

Create order:

POST /api/v1/orders
Idempotency-Key: checkout-123
Enter fullscreen mode Exit fullscreen mode

Get order:

GET /api/v1/orders/{orderId}
Enter fullscreen mode Exit fullscreen mode

Payment:

POST /api/v1/payments
Idempotency-Key: payment-123
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Inventory is more transactional:

Inventory
-------------------------
productId
warehouseId
availableQuantity
reservedQuantity
updatedAt

Primary Key:
(productId, warehouseId)
Enter fullscreen mode Exit fullscreen mode

The cart can contain:

Cart
-------------------------
cartId
userId
status
createdAt
updatedAt
expiresAt
Enter fullscreen mode Exit fullscreen mode

and:

CartItem
-------------------------
cartId
productId
quantity
priceAtAddTime
Enter fullscreen mode Exit fullscreen mode

Orders should preserve the actual price agreed during checkout:

Order
-------------------------
orderId
userId
status
subtotal
totalAmount
currency
createdAt
updatedAt
Enter fullscreen mode Exit fullscreen mode

Order items:

OrderItem
-------------------------
orderId
productId
sellerId
quantity
unitPrice
Enter fullscreen mode Exit fullscreen mode

Payment:

Payment
-------------------------
paymentId
orderId
amount
currency
status
provider
idempotencyKey
createdAt
updatedAt
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

if the common query is:

Show all orders for this user
Enter fullscreen mode Exit fullscreen mode

For example:

Shard 1 → users A–F
Shard 2 → users G–M
Shard 3 → users N–S
Shard 4 → users T–Z
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Inventory therefore becomes:

Product
   |
   ├── Bangalore → 5
   ├── Mumbai    → 10
   └── Delhi     → 2
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Each seller may ship independently.

Payment may also need to be split into:

Customer payment
      ↓
Platform commission
      ↓
Seller payouts
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A promotion might have:

Expiry
Usage limit
Eligible products
Eligible users
Minimum order value
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example:

Customers who bought A
also bought B
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A distributed rate limiter can use Redis counters.

The flow is:

Client
  ↓
API Gateway
  ↓
Rate Limiter
  ↓
Application Service
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

we can use:

User
 ↓
CDN
 ↓
Cache hit
Enter fullscreen mode Exit fullscreen mode

For a cache miss:

CDN
 ↓
Origin
 ↓
Cache
 ↓
User
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Distributed tracing can follow:

Request
  ↓
API Gateway
  ↓
Order Service
  ↓
Inventory Service
  ↓
Payment Service
  ↓
Database
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If the payment gateway is unavailable:

Payment unavailable
   ↓
Do not mark payment as successful
Enter fullscreen mode Exit fullscreen mode

If Kafka is unavailable:

Persist event in outbox
   ↓
Publish later
Enter fullscreen mode Exit fullscreen mode

If Redis is unavailable:

Use the durable database where safe
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

A reconciliation process can periodically compare authoritative inventory with derived state.

Inventory DB
     ↓
Compare
     ↓
Redis
     ↓
Repair mismatch
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Events are then published:

Transactional DB
       ↓
Kafka
       ↓
Read model builders
Enter fullscreen mode Exit fullscreen mode

Those consumers update:

Elasticsearch
Redis
Other read models
Enter fullscreen mode Exit fullscreen mode

Conceptually:

                Commands
                   |
                   ▼
              Write Model
                   |
                   ▼
              PostgreSQL
                   |
                 Kafka
                   |
          ┌────────┴────────┐
          ↓                 ↓
    Elasticsearch         Redis
      Read Model         Read Model
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Databases should use:

Replication
Backups
Automated failover
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

45. Multi-Region Architecture

As the platform becomes global, one region may no longer be enough.

We might have:

India
Europe
US
Enter fullscreen mode Exit fullscreen mode

with regional services:

India Region
   ↓
India Inventory
India Orders

Europe Region
   ↓
Europe Inventory
Europe Orders

US Region
   ↓
US Inventory
US Orders
Enter fullscreen mode Exit fullscreen mode

Inventory is particularly difficult because it is mutable state.

A product can exist in multiple warehouses:

India → 100
Europe → 50
US → 200
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The system can:

Route to healthy search replicas
       ↓
Reduce non-critical search features
       ↓
Recover Elasticsearch
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Over time:

Available inventory → artificially decreases
Enter fullscreen mode Exit fullscreen mode

A reservation expiry mechanism and reconciliation job prevent this.

For example:

Reservation created
       ↓
15-minute TTL
       ↓
Payment not completed
       ↓
Release inventory
Enter fullscreen mode Exit fullscreen mode

48. Incident: Duplicate Payment

Suppose a customer sees two payment authorizations.

We investigate using:

orderId
paymentId
idempotencyKey
provider transaction ID
Enter fullscreen mode Exit fullscreen mode

The system should:

Identify duplicate
       ↓
Void/refund duplicate
       ↓
Fix retry/idempotency behavior
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Each adds:

Infrastructure cost
Monitoring
On-call responsibility
Failure modes
Operational complexity
Enter fullscreen mode Exit fullscreen mode

For a small e-commerce application, this would be overengineering.

A much smaller system could start with:

Monolith
   ↓
PostgreSQL
   ↓
Redis
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The product catalog, cart, orders, and inventory can initially live in one application.

As traffic grows, search can be separated:

Application
    |
    ├── PostgreSQL
    ├── Redis
    └── Elasticsearch
Enter fullscreen mode Exit fullscreen mode

As asynchronous workloads grow:

Application
    |
    └── Kafka
          |
          ├── Notifications
          ├── Analytics
          └── Search indexing
Enter fullscreen mode Exit fullscreen mode

As checkout becomes more complex:

Inventory Service
Payment Service
Order Service
Enter fullscreen mode Exit fullscreen mode

can be separated.

During flash sales:

Redis atomic counters
+
Queue
+
Dedicated workers
Enter fullscreen mode Exit fullscreen mode

can protect the core transactional systems.

Finally, global expansion can introduce:

Multi-region services
Multi-warehouse inventory
Regional order processing
Disaster recovery
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

because transactions matter.

MongoDB/DynamoDB can be attractive for:

Flexible product attributes
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The database provides:

Durability
Strong source of truth
Enter fullscreen mode Exit fullscreen mode

For flash sales:

Redis → absorb the burst
Database → durable truth
Enter fullscreen mode Exit fullscreen mode

Saga vs 2PC

2PC gives stronger transactional coordination but introduces blocking and coupling.

Saga uses:

Local transactions
+
Compensation
Enter fullscreen mode Exit fullscreen mode

and fits microservice workflows better.


Synchronous vs Asynchronous

Keep critical checkout operations synchronous.

Move non-critical work to Kafka:

Email
Analytics
Recommendations
Seller notifications
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

During a flash sale, the inventory path changes:

Millions of Users
       ↓
Rate Limiter
       ↓
Queue
       ↓
Redis Atomic Counter
       ↓
Order Workers
       ↓
Inventory DB
Enter fullscreen mode Exit fullscreen mode

The architecture therefore has two very different paths.

Read path

User
 ↓
CDN / Cache
 ↓
Search / Product DB
Enter fullscreen mode Exit fullscreen mode

Optimized for:

Very high throughput
Low latency
Eventual consistency
Enter fullscreen mode Exit fullscreen mode

Transaction path

User
 ↓
Checkout
 ↓
Inventory
 ↓
Payment
 ↓
Order
Enter fullscreen mode Exit fullscreen mode

Optimized for:

Correctness
Strong consistency
Idempotency
Failure recovery
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Second, inventory is the main concurrency problem.

Many buyers
     ↓
Limited stock
     ↓
Exactly one valid reservation
Enter fullscreen mode Exit fullscreen mode

Third, checkout is a distributed transaction.

Inventory
   +
Payment
   +
Order
Enter fullscreen mode Exit fullscreen mode

Use:

Saga
+
Compensation
+
Idempotency
Enter fullscreen mode Exit fullscreen mode

Fourth, flash sales require traffic absorption.

Rate limiter
   ↓
Queue
   ↓
Redis
   ↓
Workers
   ↓
Durable DB
Enter fullscreen mode Exit fullscreen mode

Fifth, Kafka moves non-critical work away from the request path.

Order Created
      ↓
Kafka
      ↓
Notifications
Analytics
Warehouse
Recommendations
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then move to checkout:

Cart
 ↓
Inventory
 ↓
Payment
 ↓
Order
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then the flash-sale solution:

Redis atomic counter
+
Queue
+
Durable inventory database
Enter fullscreen mode Exit fullscreen mode

Then explain checkout consistency:

Saga
+
Compensation
Enter fullscreen mode Exit fullscreen mode

Then payment:

Idempotency
+
Authorization / Capture
Enter fullscreen mode Exit fullscreen mode

Then asynchronous processing:

Kafka
Enter fullscreen mode Exit fullscreen mode

Finally, discuss:

Failure recovery
Observability
Sharding
Multi-warehouse
Multi-region
Trade-offs
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And during extreme traffic:

Millions of users
       ↓
Rate Limiter
       ↓
Queue
       ↓
Redis
       ↓
Workers
       ↓
Transactional Systems
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Once those relationships are clear, the architecture becomes much easier to design from scratch in an interview.

Top comments (0)