DEV Community

Cover image for E-commerce System Design: Inventory, Checkout, Orders & Scale
MANGESH MANDLIK
MANGESH MANDLIK

Posted on Originally published at seeitflow.com

E-commerce System Design: Inventory, Checkout, Orders & Scale

ecommerce final architecture

A shopper adds the last available item to a cart. Another shopper does the same. Both click Place Order. One payment times out; the other succeeds. Meanwhile, the product price changes, the fulfillment queue is delayed, and the checkout service restarts.

A credible e-commerce system design must explain what happens next—not merely draw boxes labeled Catalog, Cart, Order, and Payment.

The central challenge is that an online store contains several workloads with different consistency requirements. Browsing and search benefit from caches and asynchronous indexing. Inventory reservations and order transitions require guarded writes. Payments involve external systems whose outcomes may remain unknown after a timeout. Fulfillment happens on a different timeline altogether.

This walkthrough builds an Amazon-like online shopping platform architecture for a general retailer. It is an illustrative design, not a claim about Amazon's internal implementation. It emphasizes the decisions and failure modes that matter in production and in a system design interview.

For the full interactive learning path, start with E-commerce Platform — Complete Design on SeeItFlow.

1. Requirements: what are we actually building?

Our baseline supports product browsing, keyword search and filters, SKU selection, guest and signed-in carts, checkout, inventory reservation, payment, order history, fulfillment tracking, cancellation, returns, and refunds.

We will initially assume a single retailer selling physical goods, with a finite stock policy and an external payment service provider. Multiple sellers, warehouse selection, sophisticated promotion engines, fraud detection, and international tax calculation can be added as explicit extensions. A marketplace changes the ownership of offers, inventory, and fulfillment; it is not merely the same architecture with more users.

Rather than assigning one latency or consistency target to the whole platform, specify them per workflow:

Workload Optimization goal Correctness boundary
Catalog Low-latency, high-volume reads Product data can be briefly stale in display
Search Relevance, filtering, independent scaling Search index is a derived discovery view
Cart Fast mutable session state Cart does not grant a stock reservation
Pricing Efficient display and quoting Checkout uses an accepted, versioned quote
Inventory Safe concurrent reservations Allocation never exceeds the defined stock policy
Checkout and orders Recoverable workflow Durable identities and guarded transitions
Payment Safe external side effects Unknown outcome is not a decline
Fulfillment Reliable asynchronous processing No duplicate fulfillment for the same operation

Availability and latency SLOs should be derived from business needs and measured load; numbers quoted in interviews are design assumptions, not universal industry guarantees.

2. Model products, SKUs, offers, and inventory separately

A Product describes an item: title, category, brand, images, and attributes. A SKU identifies a purchasable variant, such as a black T-shirt in size M. An Offer describes the commercial terms under which a merchant sells a SKU, including currency, price, validity, and seller identity when applicable.

Inventory is not a property of the abstract product. It belongs to the relevant stock pool, often keyed by SKU and fulfillment location. For a marketplace, seller-specific offers and stock ownership need additional modeling.

Product (product_id, title, attributes)
   ├── SKU A (sku_id, color=black, size=M)
   │      ├── Offer (seller, currency, price_version)
   │      └── Inventory (sku_id, location_id, on_hand, reserved)
   └── SKU B (sku_id, color=blue, size=L)
          └── ...
Enter fullscreen mode Exit fullscreen mode

An order must also preserve a snapshot of what the customer accepted: SKU, quantity, product description as needed, unit price, currency, discounts, taxes, shipping allocation, and totals. Never recalculate a historical order using today's catalog price.

This separation prevents frequent price and stock updates from requiring the same storage and cache policies as relatively stable catalog metadata.

3. Catalog architecture: cache the browsing path

A straightforward catalog read path uses a database plus a cache-aside layer. Images and other static media are served through a CDN.

catalog architecture

Browser ──> API ──> Catalog Service ──> Cache
                           │               │ miss
                           └──────────────> Catalog DB

Browser ──> CDN ──> Product images / static media
Enter fullscreen mode Exit fullscreen mode

Popular products benefit from caching, but cache hit rate depends on popularity distribution, catalog size, TTL, and capacity. Images can be updated, so version asset URLs or invalidate CDN entries rather than assuming every image URL is immutable.

Two operational details matter. First, a cache outage can suddenly redirect a large fraction of traffic to the database. Protect the origin with load shedding, circuit breakers, and capacity-aware fallbacks. Second, a hot entry expiring during a traffic spike can cause a cache stampede. Single-flight request coalescing, TTL jitter, and carefully chosen stale-while-revalidate behavior help on read-only display data.

A cached product page may show an approximate stock count or an older price. Neither value authorizes a reservation or a charge.

4. Search is a derived read model, not a second source of truth

Product search needs full-text matching, ranking, filters, and potentially typo tolerance and autocomplete. A relational database's full-text search may be sufficient initially; a dedicated search index becomes valuable when search functionality or load justifies it.

product search

Catalog DB ──> Durable change pipeline ──> Indexer ──> Search Index
                                                      │
Customer query ──> Search API ──────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Index updates are asynchronous, so the search index can lag behind the catalog. Define an indexing-lag SLO and measure it. A product may briefly appear in search after being disabled or sold out; checkout must revalidate SKU status, accepted pricing, and inventory independently.

This is a useful CQRS-style separation: the transactional catalog is authoritative, while search is optimized for discovery. It does not mean every e-commerce application needs a full CQRS framework.

5. Cart design: purchase intent is not inventory ownership

A cart normally stores sku_id, quantity, timestamps, and perhaps a displayed-price snapshot, keyed by user or guest session. A key-value store can fit frequent small updates, though a relational store is also viable.

Adding an item to the cart generally does not reserve stock. Otherwise abandoned carts could hold scarce inventory for long periods. Reservation at checkout is a sensible baseline for ordinary retail, while ticketing and limited drops may deliberately reserve earlier for a short, bounded window.

Guest-cart merging after login needs an explicit policy: combine quantities, prefer one cart, or ask the shopper. It should not silently create invalid quantities or imply that a saved cart guarantees availability.

At checkout, revalidate every SKU's purchasability, quantity, price, and availability. Cart state is a starting point, not authority.

6. Pricing: distinguish display price, checkout quote, and order price

One subtle failure is treating the price rendered on a product page as the price the payment service must charge.

There are three distinct values:

  1. Display price: a potentially cached browsing value.
  2. Checkout quote: the authoritative commercial decision for the current items, currency, applicable discounts, tax and shipping policy, with a version and/or expiry.
  3. Order line price: the accepted terms snapshotted onto the order for history and financial reconciliation.

An authoritative quote does not necessarily mean a direct database read on every request. A pricing service or appropriately versioned and invalidated cache can provide it. What matters is that the checkout decision obeys the pricing policy and quote validity rules.

If the price changes between browsing and checkout, show the updated terms and obtain acceptance as required by the product policy. Do not silently charge a different amount. Once accepted, ensure the payment amount and currency correspond to the persisted quote and order snapshot.

7. Inventory reservation: the last-item race

Suppose there is one sellable unit of SKU-42. Two shoppers attempt to buy it simultaneously. A read-then-write sequence without concurrency control is unsafe: both requests may observe one available unit and both proceed.

inventory reservation

For a simple stock pool, define:

available_to_sell = on_hand - reserved - safety_stock
Enter fullscreen mode Exit fullscreen mode

This is a simplified model. Production inventory may also distinguish damaged, allocated, in-transit, and backordered units. Some retailers intentionally permit backorders; the real invariant is that reservations do not exceed the allocation policy for the SKU and stock pool.

A relational implementation can reserve atomically:

BEGIN;

UPDATE inventory
SET reserved = reserved + :qty
WHERE sku_id = :sku_id
  AND location_id = :location_id
  AND on_hand - reserved - safety_stock >= :qty;

-- Proceed only if exactly one inventory row was updated.
-- Insert a uniquely identified reservation in the SAME transaction.
INSERT INTO reservations
  (reservation_id, checkout_id, sku_id, location_id,
   quantity, state, expires_at)
VALUES
  (:reservation_id, :checkout_id, :sku_id, :location_id,
   :qty, 'ACTIVE', :expires_at);

COMMIT;
Enter fullscreen mode Exit fullscreen mode

The application must roll back the transaction if the conditional update affects no row, if the reservation insert fails, or if any invariant check fails. A retry with the same reservation identity must return the existing reservation rather than incrementing reserved again. That requires a uniqueness constraint and idempotent request handling; the SQL sketch alone does not implement the complete retry protocol.

Other valid mechanisms include optimistic version checks, row locks, strongly consistent conditional writes, and serialized per-SKU processing. A distributed lock by itself is not a substitute for an authoritative conditional write.

A reservation is a durable entity, not merely a changed counter. Typical states are ACTIVE, COMMITTED, RELEASED, and EXPIRED. Every transition must be idempotent and guarded so that commit and expiry cannot both win.

8. Reservation expiry must survive crashes

An abandoned checkout should eventually release its held units. Store expires_at durably and enforce expiry through a recoverable mechanism: indexed database scans, a durable delayed task, or a workflow timer.

Do not rely solely on an in-memory timer or cache-key expiry notification. Those can be missed. The expiry worker should perform a conditional transition from ACTIVE to EXPIRED, update the stock accounting consistently, and tolerate retries.

There is a critical exception: a payment timeout does not prove payment failure. If the payment outcome is unknown near reservation expiry, the business needs a bounded policy—extend the hold where possible, or let it expire with a defined late-success allocation-or-refund procedure. Do not hold inventory forever, but do not casually release it on a network timeout either.

9. Checkout is a durable saga, not one global transaction

Inventory, orders, and an external payment provider cannot generally participate in one shared ACID transaction. A checkout therefore coordinates local transactions and external effects through a durable saga.

checkout sequence

A defensible baseline sequence is:

Customer
   │ Place Order (idempotency key)
   ▼
Checkout / Order Service
   │ 1. Create or reuse durable PENDING checkout/order
   │ 2. Persist accepted quote and line-item snapshot
   ▼
Inventory Service
   │ 3. Reserve stock idempotently
   ▼
Payment Service / Provider
   │ 4. Initiate or resume the same logical payment
   │    ├── confirmed success
   │    ├── definitive failure
   │    └── pending / unknown
   ▼
Checkout Orchestrator
   │ 5. Resolve payment and reservation conditions
   │ 6. Commit reservation and confirm order via
   │    guarded, recoverable cross-service workflow
   ▼
Order DB + Transactional Outbox
   │ 7. Persist CONFIRMED and order.confirmed together
   ▼
Event Broker ──> Fulfillment / Notification / Analytics
Enter fullscreen mode Exit fullscreen mode

This is a logical workflow, not one atomic operation spanning the inventory and order databases. If reservation commit succeeds but order confirmation fails, the orchestrator must resume or reconcile that intermediate state. Fulfillment must not begin merely because a payment response looked successful; it begins from a durable, valid confirmed order according to the platform's policy.

The checkout idempotency key identifies one logical attempt and maps to a durable checkout/order record. Store a request fingerprint so reusing the same key for a different cart or quote is rejected rather than silently treated as the same purchase. Separate checkout_id, order_id, reservation_id, and payment identifiers remain linked but should not be conflated.

What if the orchestrator crashes?

Record durable workflow progress and give every step a stable business idempotency identity. When execution resumes, query or reconcile the previous step's outcome before deciding whether to repeat an external side effect. Idempotency requires cooperation from each boundary, especially the payment provider; a local database key cannot by itself prevent an unrelated provider charge.

A saga's compensation is not rollback. Releasing stock, voiding an authorization, issuing a refund, or cancelling an order are separate operations that can themselves fail and need retries and reconciliation.

10. Payment success, failure, and unknown are different outcomes

An e-commerce checkout should integrate with a dedicated payment component instead of reimplementing payment-network internals.

A definitive provider decline can trigger an idempotent reservation release and a failed checkout state. A payment still processing should remain pending. A network timeout, dropped response, or delayed webhook leaves an unknown outcome, not a definitive failure.

Use a stable provider-side idempotency mechanism where supported, authenticated webhook processing, provider status queries, and reconciliation. Provider capabilities and guarantees vary by payment method and API, so avoid claiming universal exactly-once charging.

Also distinguish authorization, capture, refund, and settlement. They are different financial events. Some integrations authorize before capture; others capture immediately or complete asynchronously. An order may be confirmed according to a defined payment policy without claiming that bank settlement has already occurred.

The detailed payment state machine belongs in Payment System Design on SeeItFlow.

11. Orders, payments, and fulfillment have separate state machines

A single order_complete flag cannot represent the real lifecycle.

Domain Illustrative states
Order PENDING, CONFIRMED, CANCELLED, COMPLETED
Payment PENDING, UNKNOWN, AUTHORIZED, CAPTURED, FAILED, REFUNDED
Reservation ACTIVE, COMMITTED, RELEASED, EXPIRED
Fulfillment UNALLOCATED, ALLOCATED, PICKING, PACKED, SHIPPED, DELIVERED

These examples are not one universal state model: providers and merchants may need partial captures, multiple payment attempts, partial refunds, and per-line or per-shipment fulfillment states.

An order can be confirmed but not shipped. One order can generate several shipments. A cancellation can be accepted while the refund is still pending. Keep each domain's ownership and transitions explicit, and derive the customer-facing status from the combination rather than overwriting one boolean.

12. Transactional outbox: don't lose the fulfillment event

Consider this sequence:

1. Order DB commits CONFIRMED.
2. Process crashes before publishing order.confirmed.
3. Fulfillment never hears about the order.
Enter fullscreen mode Exit fullscreen mode

Publishing to a message broker immediately after the database commit creates a dual-write gap. The transactional outbox addresses it by inserting the order transition and an outbox event in the same local database transaction. A relay later publishes the event and retries on failure.

Order DB transaction:
  UPDATE orders SET status = 'CONFIRMED' ...
  INSERT INTO outbox (event_id, order_id, event_type, payload) ...
  COMMIT

Outbox relay ──> Broker ──> Fulfillment consumer
                           ├── Notification consumer
                           └── Analytics consumer
Enter fullscreen mode Exit fullscreen mode

The outbox protects the durability of the event record, but it does not guarantee exactly-once delivery. A relay can publish successfully and crash before recording the acknowledgment, then publish again. Consumers must deduplicate or apply operations idempotently using stable business identities.

For fulfillment, the uniqueness boundary must match the real operation—such as a fulfillment request or shipment operation per relevant order lines—not blindly order_id if partial shipments are supported. A carrier-label API also needs idempotency or an external-result reconciliation strategy, because database deduplication alone cannot undo a duplicate external shipment request.

Consumers must handle out-of-order events using order versions and guarded transitions where ordering is not guaranteed.

13. Fulfillment, cancellation, returns, and refunds

After order confirmation, fulfillment allocates a location, picks and packs goods, creates shipments, and tracks delivery. This workflow should normally be asynchronous so a slow warehouse does not block the customer's checkout response.

Cancellation eligibility depends on current fulfillment state, not the state the shopper saw seconds earlier. A cancellation request racing with shipment creation must use guarded transitions and coordination with the fulfillment owner. If the shipment has passed the cancellable point, the request may need to become a return.

Returned items should not automatically be marked sellable: inspection, damage, and disposition policy may apply. Similarly, order cancellation is not refund completion. Refunds can be pending, partially completed, failed, or ambiguous. The customer interface and reconciliation process must reflect those distinct facts.

14. Flash sales: one hot SKU defeats naive horizontal scaling

Imagine 100,000 shoppers competing for 1,000 units of the same SKU. A CDN can absorb image and catalog traffic, and stateless API instances can scale horizontally. But every successful stock reservation must still respect one authoritative inventory allocation boundary.

Adding more API servers does not eliminate contention on that hot row, partition, or reservation authority.

Possible mitigations include bounded concurrency, rate limits, exponential backoff with jitter where retries are appropriate, early sold-out rejection, and—when fairness or extreme demand requires it—a waiting room or admission tokens. At the highest contention levels, a serialized per-SKU reservation path or carefully designed inventory preallocation can help, but adds operational complexity and reconciliation needs.

A waiting room is not a correctness guarantee by itself. The final stock allocation must still be guarded. Partitioning by SKU spreads different SKUs across shards; it does not automatically distribute one extremely popular SKU.

For a deeper failure walkthrough, see E-commerce Failure & Scale Scenarios.

15. Failure scenarios worth designing before launch

Failure Unsafe reaction Safer design
Two buyers request last unit Read stock then decrement separately Atomic conditional reservation
Duplicate Place Order request Create a new order/payment each time Durable checkout identity and fingerprint
Reservation succeeds; response lost Reserve again Query/reuse same reservation identity
Payment request times out Treat as declined and release stock Mark unknown; query/webhook/reconcile
Payment succeeds; order remains pending Forget the attempt Recover from durable checkout/payment linkage
Reservation expires during confirmation Commit and release concurrently Guarded single-winner state transition
Price changes during checkout Charge whatever cart displayed Revalidate and obtain accepted quote
Order commits; publish fails Lose fulfillment event Transactional outbox
Fulfillment event redelivered Create duplicate shipment Idempotent fulfillment operation
Cancellation races with shipping Cancel based on stale display Guarded eligibility and return fallback
Inventory database unavailable Trust cached stock Pause/fail reservation safely
Region fails during checkout Allow both regions to write same stock Explicit authority, fencing, recovery and reconciliation

Regional failover deserves particular care. Replication alone does not preserve idempotency or prevent two regions from accepting conflicting reservations. Define write ownership, promotion/fencing, recovery-point objectives, and reconciliation before enabling failover. Active-active inventory is an advanced design, not a default checkbox.

16. Reconciliation catches what retries cannot

Even careful idempotency and retry handling cannot guarantee that every cross-system workflow will resolve without intervention.

A reconciliation job should look for:

  • Payment succeeded, but order remains pending.
  • Order confirmed without the expected committed reservation.
  • Reservation committed for a checkout that never confirmed.
  • Reservation expired while the payment later succeeded.
  • Outbox event stuck beyond its expected publication lag.
  • Duplicate or missing fulfillment operations.
  • Refund expected after cancellation but not completed.

Run bounded, indexed scans or event-driven exception checks; track discrepancy age and severity. Repairs must be idempotent, logged, and safe to rerun. Do not automatically recreate stock allocations, issue refunds, or create shipments whenever the true physical or financial outcome is uncertain. Some cases require quarantine and human review.

Reconciliation is a correctness mechanism, not an excuse to ignore broken invariants on the live request path.

17. Architecture decisions and trade-offs

Decision Reasonable baseline When to reconsider
Catalog search DB search initially, dedicated index when justified Complex relevance, facets, high search load
Product display Cache/CDN Personalization or strict freshness needs
Cart stock hold No reservation until checkout Scarce time-boxed inventory
Inventory concurrency Conditional atomic write Extreme hot-SKU contention
Reserve vs. pay Reserve before payment Authorization-first or domain-specific flows
Checkout coordination Durable orchestrated saga Simpler local workflow or event-driven choreography
Order timing Create durable pending identity before external payment Alternative models with equivalent recovery anchors
Downstream delivery Outbox and idempotent consumers Simpler noncritical events with accepted loss risk
Deployment Modular monolith can be sufficient Independent scale, team ownership, release cadence

These are design choices, not a mandate to deploy a dozen microservices. Clean domain boundaries can exist inside a modular monolith. Independent services become worthwhile when deployment ownership, scaling, or isolation needs justify the operational cost.

See E-commerce Trade-offs & Decisions for the full comparisons and Architecture Deep Dive for implementation mechanisms.

18. How to answer e-commerce system design interview questions

How do you prevent overselling? Use an authoritative atomic reservation per SKU/stock pool, a durable reservation identity, and guarded commit/release/expiry transitions. The display count can be stale; the reservation decision cannot be based on that stale count.

What if payment succeeds but the order service crashes? Persist a checkout/order recovery identity before the payment attempt, resolve the provider outcome using the linked payment identity, and resume confirmation or apply the predefined recovery/refund policy. Never blindly charge again.

Why not use one database transaction for checkout? An external payment provider cannot generally join the inventory/order database transaction. Use local commits coordinated by a durable saga with idempotent compensation and reconciliation.

Why do you need an outbox if you already use Kafka or another broker? Because the order database commit and the broker publish are separate writes. The outbox closes that local dual-write durability gap; it does not eliminate duplicate deliveries.

What if 100,000 people buy the last 1,000 units? Protect admission, serialize or guard the inventory allocation boundary, fail fast on sold-out inventory, and keep payment calls downstream of successful reservation. More stateless instances alone do not solve one hot stock record.

Can you cache order status? Yes, a read-only display view can be cached with clear staleness semantics. Order-state mutations still require authoritative guarded transitions.

What changes for a marketplace? Seller-scoped offers, seller/location inventory ownership, split fulfillment, merchant settlement, and additional policy boundaries. Do not assume one product has one global price or one global stock pool.

For more interviewer follow-ups, see E-commerce Interview Perspective.

19. Final architecture and takeaways

ecommerce final architecture

                           Customers
                               │
                        API / Gateway
                               │
         ┌─────────────────────┼──────────────────────┐
         │                     │                      │
   Catalog Service       Search Service         Cart Service
     │       │                 │                      │
 Catalog DB  Cache         Search Index            Cart Store
     │                         ▲
     └── Change pipeline ──────┘

                      Checkout / Order
                               │
                   Durable Saga / Order DB
                    │          │         │
               Pricing      Inventory   Payment Service
               Quote        Service     │
                             │          Payment Provider
                       Reservation DB
                               │
                       Order CONFIRMED
                               │
                    Order DB + Outbox
                               │
                         Event Relay
                               │
                            Broker
                         /     |      \
                  Fulfillment Notify  Analytics

         Reconciliation checks order, inventory,
         payment, outbox, and fulfillment outcomes.
Enter fullscreen mode Exit fullscreen mode

The most important lesson in e-commerce system design is not to make every component strongly consistent. It is to identify which decisions change money, stock, or contractual order state—and protect those boundaries precisely.

Cache catalog and search where staleness is tolerable. Keep carts as intent, not inventory ownership. Quote prices authoritatively. Reserve stock atomically. Make checkout durable and idempotent across retries. Treat payment timeouts as ambiguous. Publish confirmed orders through an outbox, make downstream consumers idempotent, and reconcile the gaps that distributed failures leave behind.

The result is an architecture that can scale browsing aggressively without trading away correctness at checkout.

Explore the complete SeeItFlow learning path: Complete Design · Architecture Deep Dive · Trade-offs & Decisions · Failure & Scale Scenarios · Interview Perspective

Top comments (0)