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.
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.
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.
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.
| Quantity | Formula | Result |
|---|---|---|
| Browse/search requests per day | 5,000,000 × 20 | 100 million/day |
| Average / peak browse rate | 100,000,000 ÷ 86,400; × 5 | ≈ 1,160 / ≈ 5,800 requests/s |
| Origin browse rate behind cache | 5,800 × (1 − 0.95) | ≈ 290 requests/s |
| Orders per day | 5,000,000 × 2% | 100,000/day (≈ 1.2/s average, ≈ 6/s peak) |
| Catalog storage | 10,000,000 × 5 KB | ≈ 50 GB |
| Order storage | 100,000 × 2 KB | ≈ 200 MB/day, ≈ 73 GB/year |
| Hot-SKU attempt rate | 500,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.
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.
| Entity | What it represents |
|---|---|
| Product | Stable, descriptive identity -- title, description, brand, category, attributes, media |
| SKU / Variant | The actual purchasable unit (e.g. black, size M) -- inventory and price attach here, not to Product |
| Offer / Price | Commercial terms for a SKU -- seller/merchant, currency, price, validity, price version |
| Inventory | Sellable stock for a SKU at a location/pool -- on-hand, reserved, available-to-sell |
| Cart | Mutable purchase intent -- can retain a displayed-price snapshot, never authoritative at checkout |
| Checkout | A durable orchestration attempt with its own idempotency identity |
| Order | The durable commercial record of accepted purchase intent, with snapshotted line items |
| Payment | A separate bounded context -- see the Payment System module for its full lifecycle |
| Fulfillment | Allocation, pick/pack, ship, deliver -- tracked per shipment, potentially more than one per order |
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.
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.
Product Search
Product search benefits from full-text matching, relevance ranking, faceted filtering and autocomplete -- capabilities a dedicated search engine provides more richly than a relational database as scale grows.
A dedicated search engine handles search queries; the catalog database remains the authoritative source, and the search index is a derived, eventually consistent read model built from catalog change events.
Indexing pipeline and freshness
When a product or SKU changes, an event triggers the indexing pipeline. Rather than promising freshness 'within minutes' universally, define an indexing-lag SLO appropriate to the product and monitor it.
Search results are discovery data, not checkout authority
A shopper searching for a product that was just added or repriced will see it once the index catches up. Whatever price or availability the search result shows, checkout always revalidates against the authoritative path -- so search staleness never becomes a checkout-correctness problem.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Final Architecture
The complete platform -- every component earns its place by solving a specific workload requirement or a specific correctness gap.
- 01API Gateway: routing, auth, rate limiting
- 02Catalog Service + DB + Cache: read-heavy product browsing, product/SKU model, versioned media
- 03CDN: product images and static assets -- substantially reduces, doesn't eliminate, origin load
- 04Search Service + Index: full-text search without taxing the catalog DB, an eventually-consistent discovery layer
- 05Pricing: authoritative checkout quotes, separate from the catalog's cached display price
- 06Cart Service + Storage: lightweight session state, no inventory hold, an optional displayed-price snapshot
- 07Inventory Service + Store: durable, identified reservations with a guarded commit/release/expiry lifecycle
- 08Checkout Orchestrator: a saga coordinating reservation, payment and order confirmation with idempotency and durable recovery
- 09Order Service + DB: an explicit state machine with snapshotted line items, separate from payment and fulfillment state
- 10Payment Service: idempotent, reconciled, with ambiguous-timeout handling -- see the Payment System module
- 11Transactional Outbox + Event Relay: closes the DB-to-event dual-write gap for reliable downstream delivery
- 12Fulfillment + Notification: async, idempotent, independently retried, don't block confirmation
- 13Reconciliation Jobs: catch payment/order/inventory/fulfillment divergence retries alone don't close