Skip to content
SeeItFlow
System Design · Architecture Deep Dive · ~26 min read

E-commerce Architecture Deep Dive

The thesis: correctness lives at a few guarded decision points (inventory reservation, checkout idempotency, order-state transitions, outbox publication), and everything around them can be cached, eventually consistent or retried. This is a layer-by-layer breakdown of every mechanism a production e-commerce platform needs -- the product/SKU/offer model, the catalog read path, pricing as its own architectural concern, the search read model, cart storage, the inventory model and its reservation lifecycle, concurrency control and expiry, checkout idempotency and orchestration, the order data model and state machine, the transactional outbox, consumer idempotency, the fulfillment workflow, reconciliation, cancellation/returns/refunds, flash-sale hot-SKU contention, partitioning and observability.

Mangesh MandlikLast updated:
Deep Dive 01

Product, SKU & Offer Model

Treating 'the product' as the single unit that inventory and price attach to is a common early modeling mistake -- it breaks the moment a product has variants.

A Product carries stable, descriptive identity: title, description, brand, category, attributes, media references. It changes rarely. A SKU (or variant) is the actual purchasable unit -- a T-shirt in black, size M is a different SKU from the same T-shirt in blue, size L. Inventory and price attach to the SKU, not to the abstract Product.

EntityKey fieldsOwns
Productproduct_id, title, description, brand, category, attributes, media refsDescriptive identity
SKU / Variantsku_id, product_id, variant attributes, statusThe purchasable unit -- attachment point for inventory and price
Offer / Pricesku_id, seller/merchant, currency, base/sale price, validity interval, price versionCommercial terms -- see Pricing Architecture below

Read/write ownership

The catalog admin/CMS write path publishes Product and SKU metadata, which changes rarely. A separate pricing/merchant path commonly owns Offer data, which can change far more often. Inventory owns per-SKU (and per-location, once multi-location is in scope) stock counts. Keeping these write paths separate keeps unrelated update rates -- rare product edits vs. frequent price or inventory changes -- from contending with each other or forcing one cache-invalidation policy to fit all three.

Deep Dive 02

Catalog Read Path

The catalog read path is cache-aside plus a CDN -- simple, but each piece has a correctness caveat worth being precise about.

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 (hit) -> catalog DB (miss only) -> CDN for images

Reads go through Catalog Service -> Cache -> Catalog DB on miss. Product images and static assets are served from a CDN independent of the cache/DB path. A write to the catalog (admin edit, price change, new product) invalidates the corresponding cache entry explicitly where correctness matters, rather than relying only on TTL expiry to eventually reflect the update.

Cache hit rate is workload-dependent

A high cache hit rate is common for popular catalogs with a long-tail access pattern, but the actual number depends on catalog size, popularity skew, cache capacity and eviction policy, TTL, and personalization. Treat any specific percentage as an illustrative example for this module, not a guaranteed baseline for every catalog.

Versioned media, not assumed-immutable media

Product images are not universally immutable. When media can change -- a corrected photo, a reshoot -- use a content-versioned URL (e.g. a path or query parameter that changes with the asset) or explicit cache/CDN invalidation, rather than assuming a permanent URL is always safe to cache forever.

CDN reduces origin load, it does not eliminate it

CDN edge caching substantially reduces repeated origin traffic for cacheable assets. It does not reduce origin load to literally zero -- cache misses, purges, geographic cold spots, and long-tail/rarely-requested assets still reach the origin.

Deep Dive 03

Pricing Architecture

The single largest content gap in a naive e-commerce design: catalog metadata and checkout price get treated as one value, when they need to be modeled, owned, and versioned separately.

Display price is a cached read used for browsing and discovery -- fine to be a few seconds or minutes stale. A checkout quote is a separate, authoritative pricing decision, validated at the moment of checkout against current commercial terms: currency, active promotions, tax and shipping if in scope, and an explicit price version.

ConceptWhat it answersWhere it lives
Display priceWhat does the product page show right now?Catalog cache / read model
Checkout quoteWhat will this customer actually be charged, right now?A versioned pricing/checkout validation step
Order line priceWhat did this customer agree to and pay?Snapshotted onto the order line, immutable

The checkout quote

A checkout quote can carry: quote_id/version, line prices, currency, discounts, tax/shipping if in scope, and an expiry. Promotions and coupons are optional inputs to the quote -- this module does not attempt to teach a full promotion-rule engine.

Order line price snapshot

Once an order is accepted, the accepted commercial terms -- unit price, currency, discounts applied -- are snapshotted onto the order line item. Never reconstruct historical order totals from today's catalog price; the catalog price today can legitimately differ from what the customer actually paid.

If price changes between cart and checkout, surface the change and require acceptance according to product policy -- never silently charge an arbitrary stale or new value. See Failure & Scale -> Price Changes During Checkout.

Deep Dive 04

Search Read Model

The search index is a derived read model built from catalog change events -- not a second copy of the database, and not the checkout authority.

Swipe to pan
Search ArchitectureCatalog DB → indexing pipeline → Search Index · separate from DBUserAPISearch SvcSearch IndexElasticsearchCatalog DBauthoritativeIndex Pipelineasyncupdates indexindexed, low-latency
Catalog change event -> indexing pipeline -> search index -- a derived, eventually consistent read model

When a product or SKU is created or updated, an event triggers the indexing pipeline, which updates the search index. The catalog database remains authoritative; the index exists for relevance, faceting, typo tolerance and independent query scaling.

Indexing lag as an SLO, not a blanket promise

Rather than promising new products appear in search 'within minutes' universally, define and monitor an indexing-lag SLO appropriate to the product (for example, an illustrative target such as 'P95 index lag under N seconds'), and alert when it's breached. See Failure & Scale -> Search Indexing Backlog.

Relational databases are not incapable of full-text search

A relational database can provide basic full-text search adequate at small catalog scale. A dedicated search engine becomes valuable as scale and requirements grow -- relevance ranking, faceted filtering, typo tolerance, autocomplete, and independent scaling away from transactional write load.

Search results are discovery data, not checkout authority

Whatever price or availability a search result displays, checkout always revalidates against the authoritative pricing and inventory path -- regardless of how fresh or stale the search index happens to be at that moment.

Deep Dive 05

Cart Storage

A cart's data model is small, but the fields it carries -- and which of them are authoritative -- matter more than the storage technology underneath it.

A cart is keyed by user ID (or a guest/session ID for unauthenticated shoppers) and holds a list of items: sku_id, quantity, and commonly a displayed-price snapshot for UX continuity, plus timestamps for expiry. A fast key-value store is a reasonable fit given the access pattern -- frequent small reads and writes, natural expiration -- but the model matters more than the specific store.

Guest vs. authenticated cart, and merge semantics

Guest carts are keyed by a session/device identifier. When a guest logs in, the guest cart and any existing authenticated cart need an explicit merge policy (merge quantities, prefer one side, prompt the user) -- this is a product decision, not a technical default.

Displayed price is a snapshot, never the authority

The cart may retain a displayed-price snapshot for UX and audit continuity, but it is never authoritative for the final charge. Checkout re-resolves the current SKU, price and availability regardless of what the cart last displayed. See Pricing Architecture above and Trade-offs & Decisions -> Price at Cart vs. Checkout.

TTL is a product decision

Cart retention -- days to weeks for B2C abandoned-cart recovery, potentially indefinite for B2B -- is a product decision the architecture accommodates, not a fixed technical constraint.

Deep Dive 06

Inventory Model

Inventory is the correctness-critical layer of the platform -- but the model needs three states, not one counter, and it is a per-SKU record, not necessarily a single global number.

StateMeaning
on_handPhysically available stock recorded in the system
reservedHeld against active checkouts/orders, not yet committed
available_to_sellon_hand - reserved (minus safety stock, if modeled)

Real inventory models commonly extend this baseline with safety stock, damaged/unsellable stock, in-transit stock, and per-location allocation once multiple warehouses or stores are in scope (see Complete Design's brief mention -- this module does not attempt a full warehouse-management design).

Oversell prevention is a policy, not universal e-commerce law

'Never oversell' is the baseline invariant this module teaches, but some businesses intentionally allow backorders or preorders. The actual invariant that generalizes is: successful reservations must not allocate more sellable units than the inventory policy for that SKU permits -- which may or may not be zero backorder tolerance.

Deep Dive 07

Inventory Reservation

A reservation needs a durable identity -- not just an atomic decrement -- so it can be found again, retried safely, committed, released or expired.

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 conditional reservation: one request succeeds, the other is rejected -- no oversell

A reservation record carries: reservation_id, checkout/order ID, sku_id, quantity, state, expires_at, and a version/idempotency key. The reserve operation and the reservation record should be coordinated transactionally where the underlying store supports it.

sqlOne valid single-database implementation of a conditional reserve
UPDATE inventory
SET reserved = reserved + :qty,
    version  = version + 1
WHERE sku_id = :sku_id
  AND (on_hand - reserved) >= :qty
  AND version = :expected_version;

-- In the same transaction: insert the reservation row
INSERT INTO reservations (reservation_id, checkout_id, sku_id, qty, state, expires_at)
VALUES (:reservation_id, :checkout_id, :sku_id, :qty, 'ACTIVE', :expires_at)
ON CONFLICT (reservation_id) DO NOTHING;

The reserve operation must be idempotent

A retried reserve request carrying the same checkout/reservation identity must not create a second reservation -- it should recognize the existing ACTIVE reservation and return it, not double-reserve the SKU.

Lifecycle

ACTIVE transitions to COMMITTED once the order reaches the point where the business considers the sale final, or to RELEASED/EXPIRED on definitive cancellation, definitive payment failure, or timeout.

Commit and release must each be idempotent too

Not only the initial reserve -- committing an ACTIVE reservation (on order confirmation) and releasing one (on definitive failure, cancellation, or expiry) are each separate operations that can be retried or redelivered. A retried commit against an already-COMMITTED reservation, or a retried release against an already-RELEASED one, must resolve to a safe no-op rather than erroring out or double-applying the transition.

Deep Dive 08

Inventory Concurrency Control

There is more than one valid way to implement the same atomic check-then-reserve invariant -- the right choice depends on contention level and store.

MechanismHow it worksGood fit for
Conditional atomic updateUPDATE ... WHERE available >= qty in one statementOrdinary contention -- the default baseline for most SKUs
Optimistic versioningRead version, write with WHERE version = expected, retry on conflictModerate contention where retries are cheap and rare
Row lockSELECT ... FOR UPDATE, then check-and-write inside the transactionCases needing multiple related checks/writes in one transaction
Serialized per-SKU processingFunnel all reservation attempts for one SKU through a single ordered pathExtreme contention on one hot SKU -- see Flash-Sale Hot-SKU below
sql
-- One transaction (PostgreSQL syntax). reservation_id is the idempotency identity.
INSERT INTO reservations (reservation_id, sku_id, qty, state, expires_at)
VALUES (:reservation_id, :sku_id, :qty, 'ACTIVE', :expires_at)
ON CONFLICT (reservation_id) DO NOTHING;      -- a retry inserts 0 rows: stop here

UPDATE inventory
   SET reserved = reserved + :qty
 WHERE sku_id = :sku_id
   AND on_hand - reserved >= :qty;              -- available_to_sell guard
-- 0 rows updated => ROLLBACK and report sold out; 1 row => COMMIT
Deep Dive 09

Reservation Expiry

Expiry has to be durable and recoverable -- an abandoned checkout must reliably release its reservation even if nothing else about the system reminds it to.

MechanismHow it works
Scheduled expiry scanA periodic job scans ACTIVE reservations past expires_at and releases them
Delayed messageA release message is scheduled at reservation time via a delay queue or task scheduler and fires later
Expiry indexAn index on expires_at makes the scan above efficient at scale
Workflow timerIf a durable workflow engine backs checkout, its own timer mechanism fires the release step

TTL is a backstop signal, not the correctness mechanism

A cache TTL or expiry notification can opportunistically trigger cleanup, but correctness cannot depend solely on an unreliable expiry signal. The reservation record's own durable expires_at field, combined with one of the recovery mechanisms above, is what actually guarantees release.

Deep Dive 10

Checkout Idempotency

A stable checkout idempotency identity is what maps repeated Place Order submissions to one logical checkout/order instead of creating duplicates.

A client-generated idempotency key (or a server-issued checkout session ID handed to the client at cart-to-checkout transition) maps to a durable uniqueness constraint. Store a request fingerprint -- cart contents, quote version -- alongside the key so a conflicting reuse of the same key (different cart contents under the same key) can be detected and rejected rather than silently applied.

Deep Dive 11

Checkout Orchestration

Checkout is a saga -- a sequence of local commits across several services, coordinated by durable workflow state rather than one shared ACID transaction.

Swipe to pan
Checkout SequenceCart → inventory reserve → order → payment → confirmUserCartOrder SvcInventoryatomic reservePaymentidempotentOrder DBCONFIRMED1. reserve2. charge3. confirm
Checkout sequence: cart -> inventory reserve -> order -> payment -> confirm, each step recorded before the next begins

A useful baseline state machine: CHECKOUT_CREATED -> INVENTORY_RESERVED -> PAYMENT_PENDING -> CONFIRMED, with explicit failure and unknown branches (INVENTORY_UNAVAILABLE, PAYMENT_FAILED, PAYMENT_PENDING_UNKNOWN) rather than only a happy-path chain.

Durable workflow state

Each transition is recorded in durable storage before the next step is triggered, so an orchestrator crash mid-checkout can resume from the last recorded state instead of depending on one in-memory thread remembering what happened.

A workflow engine is optional, not required

A dedicated durable workflow engine is one valid implementation. A well-modeled state machine inside the checkout/order table, with guarded transitions and a resumable step, works too -- the requirement is durability and recoverability, not a specific product.

Deep Dive 12

Checkout Compensation

Compensation is a fallible business operation performed after the fact -- not an automatic rollback the saga provides for free.

Typical compensating actions: release the inventory reservation, refund or void the payment, cancel the order. Each is a real operation against a real system, with its own possible failure.

Compensation can fail too

Example: payment definitively fails, and the compensating inventory-release call itself fails (network error, inventory service unavailable). The system retries the compensation idempotently, and reservation expiry plus reconciliation act as the backstop -- the design never assumes compensation succeeded merely because it was invoked.

Deep Dive 13

Order Data Model

An order is a header plus an immutable, snapshotted set of line items -- it must never need to reconstruct itself from mutable catalog or pricing data.

Order headerLine item (snapshotted)
order_id, checkout_id, customer_id, status, currency, totals, created_atsku_id, product title/description snapshot as needed, unit_price, currency, quantity, discount/tax/shipping allocation if in scope
Deep Dive 14

Order State Machine

Order state, payment state and fulfillment state are three separate state machines -- collapsing them into one enum produces invalid combinations.

DomainExample states
OrderCREATED, PAYMENT_PENDING, CONFIRMED, CANCELLED, COMPLETED
Payment (owned by Payment System)pending, unknown, succeeded, failed, refunded
FulfillmentUNALLOCATED, ALLOCATED, PICKING, PACKED, SHIPPED, DELIVERED, CANCELLED

Valid combinations that a single boolean can't express

An order can be CONFIRMED while fulfillment has not started; partially shipped while other lines are still picking; or paid while a cancellation/refund is already in progress. A single order_complete boolean cannot represent any of these -- three separate, coordinated state machines can.

Deep Dive 15

Transactional Outbox

Committing the order's CONFIRMED state to the database and publishing order.confirmed to a broker are two separate operations against two separate systems -- and that gap silently drops or duplicates events if left unaddressed.

Swipe to pan
Async Order ProcessingEvent queue decouples checkout from fulfillment + notificationsOrder SvcCONFIRMEDEvent QueueFulfillmentNotificationsAnalyticsorder.confirmedeach retriesindependently
Event queue decouples checkout from fulfillment -- but the DB-to-queue hand-off itself needs a durability mechanism

Failure window: the order DB commits CONFIRMED, the process crashes before the publish call, and no fulfillment event is ever produced. The fix: in the same database transaction that writes the order transition, also insert a durable outbox row representing the event. A separate relay reads unpublished outbox rows and publishes them to the broker, marking them published once acknowledged.

Because the relay can itself crash between publishing and marking a row published, it may republish the same event on restart -- the outbox closes the durability gap, it does not eliminate the need for idempotent consumers (see Event Consumer Idempotency below).

Deep Dive 16

Event Consumer Idempotency

At-least-once delivery is the guarantee actually being provided -- every downstream consumer of order events needs to tolerate duplicates by design.

Fulfillment consumer

A stable business idempotency key, conceptually UNIQUE(order_id, fulfillment_operation), ensures a duplicate order.confirmed event does not create a duplicate shipment.

Notification consumer

Deduplicate on a stable message/business identity where sending a duplicate confirmation email or push notification matters to the product.

Analytics consumer

Duplicate events distort aggregate metrics if not deduplicated, or the aggregation itself needs to be designed to tolerate duplicates explicitly (e.g. counting distinct order IDs rather than raw event counts).

Deep Dive 17

Fulfillment Workflow

Fulfillment is a real stateful workflow with its own transitions -- not a generic async consumer that just marks the order 'shipped.'

A fulfillment request is created off the order.confirmed event, then moves through allocation (assigning a location/warehouse to fulfill from), pick, pack, shipment, and delivery-status tracking. An order can ship in more than one package -- partial shipment is a normal, not exceptional, path.

Deep Dive 18

Order Reconciliation

A distributed checkout can accumulate inconsistencies even when every individual service retries correctly -- reconciliation is the sweep that catches what retries alone didn't resolve.

DiscrepancyWhat it looks like
Paid but unconfirmedPayment succeeded; order still PAYMENT_PENDING
Confirmed without reservationOrder CONFIRMED; no COMMITTED inventory reservation backing it
Expired reservation, active checkoutReservation released by TTL while the checkout believed it still held stock
Duplicate fulfillmentMore than one shipment created for the same order line
Refund expected, not completedOrder CANCELLED; payment refund still pending or missing
Stuck outbox eventAn outbox row written but never marked published past a normal relay lag window

Reconciliation properties

Idempotent (a re-run of the same batch is a no-op for already-repaired cases), observable (every discrepancy and repair is logged), bounded/batched (never one giant unindexed scan), safe to rerun, and able to quarantine cases that need manual/human review rather than auto-repairing every discrepancy -- financial and physical ambiguities in particular often need explicit review policy rather than automatic correction.

Deep Dive 19

Cancellation, Returns & Refunds

Cancellation and return are their own distributed workflows -- not a simple state flag -- and refund is a separate financial operation owned by the Payment System.

Cancellation before fulfillment starts: validate the order is still in an eligible state, cancel the fulfillment request if it hasn't started, release or restore inventory according to the inventory model's semantics, and void or refund payment as required. After shipment, cancellation typically becomes a return/refund workflow instead.

Returns

Return authorization and receipt are tracked at a high level. Returned goods often need inspection before becoming sellable again -- they do not necessarily restock immediately on receipt.

Refunds are independent of order cancellation

A refund is owned by the Payment System and can itself be pending, failed, or ambiguous -- see the Payment System module for its full lifecycle. Order = CANCELLED and refund = SUCCEEDED are two separate facts in two separate state machines; never equate them, and never report a cancellation as financially complete until the refund state actually resolves.

Deep Dive 20

Flash-Sale Hot-SKU Contention

Horizontal scaling of a stateless Inventory Service does not remove contention on one scarce SKU -- the bottleneck is a single row or partition, not compute capacity.

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
Browse traffic scales via CDN/cache; inventory reservation for one scarce SKU stays a serialized, correctness-critical operation

Browse traffic during a flash sale can scale through CDN and cache. Checkout admission for the scarce SKU cannot -- every reservation attempt for that one SKU ultimately contends on the same row or partition, regardless of how many stateless service instances are running.

Aggressive client retries make contention worse

Retrying every failed contender immediately amplifies load on the exact hot row that's already contended -- bounded retry with backoff is required, not unconditional immediate retry.

Waiting room / virtual queue

Smooths admission into the checkout path when business requirements justify it (extreme scarcity, ticketing-like demand). It is an optional pattern, not a requirement for ordinary commerce.

Per-SKU serialized reservation processing

An alternative for extreme contention: funnel every reservation attempt for one hot SKU through a single ordered processing path, trading some latency for the elimination of contention-driven retries.

Partitioning distributes SKUs, not one SKU

Partitioning by SKU spreads different SKUs across shards, but it cannot split one indivisible hot SKU without additional design -- sub-partitioning the available count, or a reservation-counter/preallocation scheme, can go further, at the cost of its own added reconciliation complexity.

Fail fast once sold out

'Sold out' should stop unnecessary downstream payment traffic quickly, rather than let doomed checkout attempts reach the payment provider after the SKU is already exhausted.

Deep Dive 21

Partitioning

Catalog, cart, inventory and orders don't need to share one partition key -- each workload's access pattern picks a different natural key.

DomainCommon partition keyWhy
Catalogproduct/SKU ID (or category)Read-heavy, easy to fan out
Cartuser/session IDNaturally scoped to one shopper
InventorySKU ID (plus location, if multi-location)Isolates contention per SKU
Ordersorder ID, with customer/merchant secondary indexesWrite-heavy, but also needs customer- and merchant-facing lookups
Deep Dive 22

Observability

Metrics should be separated by workload boundary the same way the architecture is -- a search indexing lag spike and an inventory reservation conflict spike are different incidents, even though both can look like 'the site is slow' to a customer.

MetricWhy it matters
Browse / search latencyRead-path health, separate from checkout-path health
Cache hit/miss rateCatalog cache effectiveness and origin-load pressure
Search indexing lagDiscovery freshness, tracked against its own SLO
Add-to-cart / checkout conversionFunnel health, a product signal as much as a technical one
Inventory reservation conflict/failure rateContention level on scarce SKUs
Reservation age / expiry rateAbandoned-checkout volume and reservation-hold pressure
Checkout workflow ageStuck or slow-moving sagas
Payment pending/unknown countAmbiguous payment volume awaiting resolution
Order confirmation lagTime from payment resolution to order CONFIRMED
Outbox lagTime between a DB commit and the event actually reaching the broker
Consumer lagHow far behind fulfillment/notification/analytics consumers are
Fulfillment backlogWarehouse-side processing capacity vs. incoming confirmed orders
Reconciliation mismatchesLeading indicator of a systemic integration issue, not just isolated incidents
Oversell / backorder incidentsDirect measure of the platform's core correctness invariant
Cancellation/refund lagTime from cancellation request to refund resolution