Skip to content
SeeItFlow
System Design · 16 chapters · ~32 min read

Designing an E-commerce Platform

An e-commerce platform is not one homogeneous system — product browsing, search, pricing, inventory, checkout, payment, and fulfillment each have fundamentally different consistency, latency, and scaling requirements. The hard part is preserving business invariants (never oversell a SKU, never double-charge) across all of them even though they can't share one global ACID transaction. Every component in this walkthrough earns its place by solving a specific workload problem.

Mangesh MandlikLast updated:
Chapter 01

Problem Definition

We are designing an e-commerce platform where users browse products, search by keyword, add items to cart, check out, pay, and receive confirmation, fulfillment and delivery of their orders -- with cancellation and returns as part of the lifecycle, not an afterthought.

The architectural challenge is not handling any single one of these operations -- it is recognizing that each requires a different architectural strategy, and that correctness has to be preserved across all of them even though none of them can share one database transaction. The same approach that makes product browsing fast would cause inventory oversell. The same database that stores orders cannot efficiently power full-text search.

The central lesson

An e-commerce platform contains at least six distinct workloads: (1) product catalog browsing -- read-heavy, cacheable, eventually consistent; (2) search -- text-optimized, indexed, fresh against its own SLO rather than real-time; (3) pricing -- a commercial decision that must be authoritative at checkout even though it's displayed from a cache elsewhere; (4) inventory -- correctness-critical, guarded at the reservation decision; (5) cart and checkout -- session state plus a cross-service orchestration with distributed failure handling; (6) order, payment and fulfillment -- a durable business record, an external financial workflow, and an asynchronous physical workflow, each with its own state machine.

Interactive

Visual Design Journey

Watch one e-commerce platform evolve from a single database to a system where catalog browsing, search, inventory, checkout, and fulfillment each get the consistency and scaling guarantees they actually need — including what happens when two shoppers race for the last unit.

Chapter 02

Requirements

Functional requirements define what users can do. Non-functional requirements differ dramatically per workload -- there is no single platform-wide consistency or latency rule that fits all of them.

Functional

Browse the catalog and categories. Search and filter products. View product/SKU details. Add, update and remove cart items. Validate price and availability. Initiate checkout idempotently. Reserve inventory. Create and confirm an order. Make and resolve payment. View order status. Cancel where permitted. Return and refund at least conceptually. Track fulfillment and shipping.

Non-functional, per workload

Low browse/search latency

Users abandon slow pages. Catalog reads and search results should be fast -- an illustrative target such as sub-100ms catalog reads and sub-200ms search is a reasonable example SLO to state in an interview, not a universal guarantee every catalog must hit.

Guarded inventory reservation

No allocation beyond the SKU's own inventory policy. The reservation decision -- not display-time inventory -- is the correctness boundary; a stale read at that specific decision point is what causes oversell.

Authoritative checkout pricing

The price a customer is actually charged must come from an authoritative, versioned quote -- not from whatever the cart or catalog cache last displayed.

Reliable order creation

Once the confirmation conditions are satisfied, the order must be durably created and safely resumable -- partial failures anywhere in checkout need explicit compensating actions, not an assumed rollback.

Safe payment processing

Idempotent, with retry, ambiguous-timeout handling, and reconciliation. See the Payment System module for the full treatment; this module owns the checkout/order/inventory boundary around it.

Async downstream processing

Fulfillment and notification must not block checkout confirmation. A slow warehouse system should not affect the checkout experience.

Capacity estimate (illustrative)

These are illustrative planning numbers, not measurements of any real system. Assumptions: 5 million daily active users; 20 catalog/search page requests per user per day; a CDN and cache layer that serves 95% of browse requests; a 2% order conversion per daily active user; peak-to-average factor 5; 10 million SKUs at about 5 KB of catalog data each (images excluded); about 2 KB per order; and, for the flash sale, 1,000 units of one SKU with 500,000 users attempting to buy in the first 60 seconds. Day = 86,400 s.

QuantityFormulaResult
Browse/search requests per day5,000,000 × 20100 million/day
Average / peak browse rate100,000,000 ÷ 86,400; × 5≈ 1,160 / ≈ 5,800 requests/s
Origin browse rate behind cache5,800 × (1 − 0.95)≈ 290 requests/s
Orders per day5,000,000 × 2%100,000/day (≈ 1.2/s average, ≈ 6/s peak)
Catalog storage10,000,000 × 5 KB≈ 50 GB
Order storage100,000 × 2 KB≈ 200 MB/day, ≈ 73 GB/year
Hot-SKU attempt rate500,000 ÷ 60 s≈ 8,300 reservation attempts/s on one SKU

What this implies for the design: browse is the large number (thousands per second) but the CDN and cache turn it into a few hundred origin requests per second, so it is a caching problem. Checkout is tiny (single-digit orders per second at peak), so aggregate checkout throughput is not the constraint. The hard shape is one SKU: about 8,300 attempts/s converge on one row or partition for 1,000 units, which is why reservation uses an atomic conditional update and why a flash sale needs admission control. Assumption: one contended conditional update takes about 1 ms, so one SKU serialises at roughly 1,000 reservations/s. Admitting about 3× the unit count as candidates (3,000, to cover payment failures and expiries) then takes about 3 s; everyone else gets a fast sold-out or waiting-room response before reaching payment.

Chapter 03

Core Domain Model

Before any implementation detail, it's worth naming the entities this whole design revolves around -- they reappear in every chapter that follows.

EntityWhat it represents
ProductStable, descriptive identity -- title, description, brand, category, attributes, media
SKU / VariantThe actual purchasable unit (e.g. black, size M) -- inventory and price attach here, not to Product
Offer / PriceCommercial terms for a SKU -- seller/merchant, currency, price, validity, price version
InventorySellable stock for a SKU at a location/pool -- on-hand, reserved, available-to-sell
CartMutable purchase intent -- can retain a displayed-price snapshot, never authoritative at checkout
CheckoutA durable orchestration attempt with its own idempotency identity
OrderThe durable commercial record of accepted purchase intent, with snapshotted line items
PaymentA separate bounded context -- see the Payment System module for its full lifecycle
FulfillmentAllocation, pick/pack, ship, deliver -- tracked per shipment, potentially more than one per order
Chapter 04

Product Catalog

The product catalog is the most read-heavy component. Products are browsed far more often than they're updated -- but inventory and price live on the SKU underneath, not on the Product itself.

Swipe to pan
Catalog Read PathUser → API → Cache (hit) → Catalog DB (miss only)UserAPICatalog SvcCachecache-asideCatalog DBmiss onlyCDNimagesstatic assetsmiss
Catalog read path: user -> API -> cache -> catalog DB on miss

A Product carries ID, title, description, category and attributes; a SKU underneath it carries the variant, inventory and price. Product metadata changes rarely, which makes catalog reads ideal for caching -- images and price are a different story, covered next.

Cache hit rate is workload-dependent

A high cache hit rate is common for popular catalogs, but the actual number depends on catalog size, popularity skew, cache capacity and TTL -- treat any specific percentage as illustrative, not a guarantee every catalog will see.

CDN for images -- reduces, not eliminates, origin load

Product images are commonly cache-friendly, but not universally immutable -- if media can change (a corrected photo), use a versioned URL or explicit invalidation. CDN edge caching substantially reduces origin traffic for cacheable image requests; it does not reduce it to zero.

Chapter 06

Pricing

Pricing deserves its own architectural treatment -- catalog metadata and checkout price are not one value, even though they're often taught that way.

The product page shows a display price, served from the catalog cache -- fine for browsing. Checkout resolves a separate, authoritative quote: current currency, active promotions if any, and an explicit price version, valid for a bounded window.

Price snapshot on the order

Once an order is accepted, the accepted price is snapshotted onto the order line -- the order never needs to reconstruct historical totals from today's catalog price, which may have since changed.

Chapter 07

Inventory

Inventory is correctness-critical. Unlike product names and descriptions, inventory counts change on every purchase -- and the reservation decision, not the display count, is where correctness actually lives.

Swipe to pan
Atomic Inventory ReservationConditional decrement — one succeeds, one fails. No oversell.User AUser BInventory SvcInventory StoreACID · 1 unitreserve 1reserve 1User A: RESERVED ✓ | User B: REJECTED ✗UPDATE SET reserved+1 WHERE available - reserved > 0
Atomic inventory reservation: conditional decrement prevents oversell

on_hand, reserved, available_to_sell

An inventory unit is tracked across three figures: on_hand (physically available), reserved (held during checkout), and available_to_sell (on_hand minus reserved). The reservation itself is a durable record with an identity, quantity, state and expiry -- not just a decremented counter.

Atomic reservation

The reservation operation is atomic: reserve a quantity only if available_to_sell is sufficient. This cannot be split between two concurrent requests -- one succeeds, and if the count reaches zero, all others fail with an informative message.

No caching for the reservation decision

An approximate inventory count can be shown on the product page, refreshed periodically. The reservation decision itself always goes through the authoritative inventory store -- no cached value is ever authoritative for that specific decision.

Chapter 08

Cart

A cart is a user's expressed intent to purchase. It is not a reservation. Adding to cart must not hold inventory.

Cart data is: SKU/variant identity and quantity per line, associated with a user or guest session, and commonly a displayed-price snapshot for UX continuity. It is lightweight, frequently read and written, and naturally expires -- a fast key-value store is a reasonable fit for that access pattern.

Cart vs. reservation

If every cart addition held inventory, popular products would appear perpetually sold out -- held by shoppers who may never complete checkout. Inventory is reserved only when the shopper commits to purchase, at checkout initiation.

Price and availability are re-resolved at checkout

The cart's displayed-price snapshot is never authoritative. At checkout, current price and availability are looked up fresh -- this prevents a shopper from paying a price that changed since they added the item.

Chapter 09

Checkout & Order

Checkout combines cart validation, inventory reservation, order creation and payment -- a multi-step sequence spanning several systems with no shared ACID transaction. This is the chapter where ambiguity is most costly, so the baseline ordering has to be explicit.

Swipe to pan
Checkout SequenceCart → inventory reserve → order → payment → confirmUserCartOrder SvcInventoryatomic reservePaymentidempotentOrder DBCONFIRMED1. reserve2. charge3. confirm
Checkout sequence: cart -> inventory reserve -> order -> payment -> confirm

Three identities

Checkout tracks three distinct identities that may be created at different points but must stay durably linked: a checkout_id (the orchestration attempt), an order_id (the durable commercial record), and a payment_id (the financial operation, owned by the Payment System).

A clear baseline sequence

(1) the client submits checkout with an idempotency key; (2) a durable checkout/order is created or reused in a PENDING state; (3) line items and accepted commercial terms are snapshotted; (4) inventory is reserved idempotently; (5) payment is initiated and resolved; (6) the order transitions to CONFIRMED only once the required conditions are actually satisfied; (7) the reservation is committed according to the inventory model; (8) an outbox event is written atomically with the order transition; (9) fulfillment starts asynchronously. The exact ordering has trade-offs -- see Trade-offs & Decisions -> Order-Before-Payment vs. Order-After-Payment -- but this baseline resolves the ambiguity the diagram alone can't.

Chapter 10

Saga / Workflow

Naming the pattern matters: checkout is a saga, and a saga does not provide ACID rollback -- it provides compensation, which is its own fallible operation.

Each service commits locally; durable workflow state records progress; failures trigger retries or compensations; compensations can themselves fail and must be idempotent and retriable; reconciliation handles workflows that get stuck despite all of that.

Chapter 11

Payment

Payment is kept bounded here -- the full mechanics live in the Payment System module. This module owns the e-commerce-specific responsibilities around that boundary.

Stable order/payment linkage

The order and its payment attempt(s) are durably linked through the checkout identity, so recovery never has to guess which order a given payment belongs to.

Definitive failure vs. ambiguous status

A definitive decline releases the reservation and moves the order to a payment-failed state. A timeout or otherwise ambiguous outcome does not -- it is never automatically treated as a failure, because the payment may have actually succeeded.

Never create a second independent payment on retry

A retried checkout resolves to the same checkout/order identity and, through it, the same payment attempt -- it never triggers a second, independent charge.

Chapter 12

Async Order Processing

After a successful checkout, fulfillment, notifications and analytics must not block the confirmation response -- and the hand-off from the order database to the event queue has its own failure window worth naming.

Swipe to pan
Async Order ProcessingEvent queue decouples checkout from fulfillment + notificationsOrder SvcCONFIRMEDEvent QueueFulfillmentNotificationsAnalyticsorder.confirmedeach retriesindependently
Event queue decouples order confirmation from downstream processing

In the same database transaction that writes the CONFIRMED transition, the Order Service also writes a durable outbox event; a separate relay publishes it to the queue. Fulfillment, notification and analytics consume it independently, each retrying its own failures without affecting the customer's confirmation or each other.

Chapter 13

Cancellation, Returns & Refunds

Cancellation and returns are part of the core lifecycle, not an edge case bolted on afterward -- and refund is a separate financial operation, not the same fact as a cancelled order.

Cancelling before fulfillment starts: validate the order is still eligible, cancel the fulfillment request if it hasn't begun, release or restore inventory per the inventory model, and void or refund payment as required. After shipment, cancellation typically becomes a return workflow instead -- authorization, receipt, and often an inspection step before returned stock becomes sellable again.

Chapter 14

Peak Traffic & Flash Sales

During a flash sale, browse traffic spikes sharply and CDN/cache absorb most of it -- but inventory reservation for the scarce SKU stays correctness-critical regardless of traffic, and horizontal API scaling alone does not remove contention on that one SKU.

Swipe to pan
Flash Sale — Cache vs CorrectnessCDN + cache absorb browse load · inventory stays real-timeUserstraffic spikeCDNimages ✓Cachecatalog ✓Catalog DBlow loadInventory Svcreal-time ⚡miss onlymissNo cache for checkoutreservation = atomic + real-time
CDN + cache absorb browse load; inventory reservation stays real-time and correctness-critical

CDN + cache absorb browse load

Product images, descriptions and static assets are served from CDN edge nodes and cache; origin systems see only a fraction of total browse traffic.

The scarce SKU is a storage-level bottleneck, not a compute one

Every reservation attempt for one hot SKU ultimately contends on the same row or partition, no matter how many stateless service instances are running. Bounded/backoff retry, admission control (a waiting room where the product genuinely needs one), and serialized per-SKU processing are the responses -- see Architecture Deep Dive -> Flash-Sale Hot-SKU Contention.

Fail fast once sold out

Once a SKU is exhausted, checkout should reject quickly, before reaching the payment step -- this avoids sending doomed traffic to the payment provider and keeps the correctness-critical path focused on requests that can actually succeed.

Chapter 15

Reconciliation

Even with idempotency, retries and compensation everywhere, a distributed checkout can still accumulate small inconsistencies -- reconciliation is the backstop that catches them.

A periodic, idempotent sweep checks for gaps retries alone don't close: payment succeeded but the order never confirmed, a confirmed order with no committed reservation behind it, an expired reservation for a checkout that's still active, a stuck unpublished outbox event, or a refund that never completed.

Chapter 16

Final Architecture

The complete platform -- every component earns its place by solving a specific workload requirement or a specific correctness gap.

Swipe to pan
Final ArchitectureEvery component traces to a workload requirementUserAPI GWCatalogCacheCatalogDBCDNSearchSearchIdxCartCartStoreInventoryOrderPaymentQueueFulfil/Notify
Full e-commerce architecture -- browse, search, pricing, cart, checkout, inventory, orders, async processing
Key takeaways
  1. 01API Gateway: routing, auth, rate limiting
  2. 02Catalog Service + DB + Cache: read-heavy product browsing, product/SKU model, versioned media
  3. 03CDN: product images and static assets -- substantially reduces, doesn't eliminate, origin load
  4. 04Search Service + Index: full-text search without taxing the catalog DB, an eventually-consistent discovery layer
  5. 05Pricing: authoritative checkout quotes, separate from the catalog's cached display price
  6. 06Cart Service + Storage: lightweight session state, no inventory hold, an optional displayed-price snapshot
  7. 07Inventory Service + Store: durable, identified reservations with a guarded commit/release/expiry lifecycle
  8. 08Checkout Orchestrator: a saga coordinating reservation, payment and order confirmation with idempotency and durable recovery
  9. 09Order Service + DB: an explicit state machine with snapshotted line items, separate from payment and fulfillment state
  10. 10Payment Service: idempotent, reconciled, with ambiguous-timeout handling -- see the Payment System module
  11. 11Transactional Outbox + Event Relay: closes the DB-to-event dual-write gap for reliable downstream delivery
  12. 12Fulfillment + Notification: async, idempotent, independently retried, don't block confirmation
  13. 13Reconciliation Jobs: catch payment/order/inventory/fulfillment divergence retries alone don't close