System Design Problems
22 min read

Design Amazon Shopping Cart

A system design for an e-commerce shopping cart handling millions of concurrent users, real-time inventory, dynamic pricing, and distributed checkout. This design focuses on high availability during flash sales, consistent inventory management, and seamless guest-to-user cart transitions.

Async Processing

Data Layer

Application Services

Edge & Gateway

Client Layer

Web App

Mobile App

CDN / CloudFront

Load Balancer

API Gateway

Cart Service

Pricing Service

Inventory Service

Checkout Orchestrator

Payment Service

Notification Service

Redis Cluster

Cart DB

Inventory DB

Order DB

Message Queue

Background Workers

High-level shopping cart architecture showing client apps, gateway layer, application services, and data stores with async processing for cart expiration and abandoned cart recovery.

Shopping cart systems must solve three interconnected problems: cart state persistence (guest vs authenticated users, cross-device sync, cart merge on login), inventory accuracy (preventing overselling during concurrent access while maintaining availability), and checkout atomicity (coordinating payment, inventory deduction, and order creation across distributed services).

The core architectural decisions:

  1. Two-tier cart storage: Redis for sub-millisecond reads during browsing; persistent database for durability and cross-device sync
  2. Soft reservations with expiry: Reserve inventory on add-to-cart (5-minute TTL), convert to hard reservation only after payment confirmation—prevents inventory lock-up from abandoned carts
  3. Saga pattern for checkout: Orchestrated sequence of payment authorization → inventory hard reservation → order creation, with compensating transactions on failure
  4. Eventually consistent inventory display, strongly consistent checkout: Accept stale counts on product pages for availability; enforce consistency only during checkout
DimensionOptimizes ForSacrifices
Redis cart cacheRead latency (<1ms)Durability (requires DB backup)
Soft reservationsInventory turnoverCheckout may fail if reservation expires
Saga orchestrationReliability, debuggabilityLatency (sequential steps)
Hash-based shardingEven distributionCross-user queries
FeatureScopeNotes
Add/remove/update cart itemsCoreReal-time quantity validation against inventory
Guest cart with persistenceCoreSurvives browser close, 30-day expiry
Cart merge on loginCoreCombine guest + user cart, resolve quantity conflicts
Real-time price updatesCorePrice at checkout reflects current price, not add-time
Inventory soft reservationCorePrevent checkout failures from out-of-stock
Coupon/promotion applicationCoreStackable rules, exclusivity handling
Multi-step checkoutCoreAddress → Payment → Review → Confirm
Abandoned cart recoveryExtendedEmail/push notifications with cart link
Wishlist/Save for LaterExtendedMove items between cart and wishlist
RequirementTargetRationale
Availability99.99% (4 nines)Revenue-critical; 1 hour downtime = millions in lost sales
Cart read latencyp99 < 50msInstant feedback on cart interactions
Cart write latencyp99 < 200msAcceptable for add/remove operations
Checkout latencyp99 < 3sEnd-to-end including payment processing
Peak throughput100K cart ops/secFlash sale scenarios
Data durability99.999999999%Cart data must not be lost
ConsistencyEventual (display), Strong (checkout)Hybrid model per operation type

Users:

  • DAU: 50M
  • Peak concurrent users: 5M (10% of DAU during flash sales)
  • Carts per user: 1 active

Traffic:

  • Cart views: 50M DAU × 10 views/day = 500M/day = ~6K RPS average
  • Cart modifications: 50M DAU × 3 ops/day = 150M/day = ~1.7K RPS average
  • Peak multiplier (flash sale): 50x → 300K cart views/sec, 85K modifications/sec
  • Checkouts: 5M/day = ~60 checkouts/sec average, 3K/sec peak

Storage:

  • Cart record: ~2KB (metadata + 10 items average)
  • Active carts: 50M × 2KB = 100GB
  • Historical carts (90-day retention): 100GB × 3 = 300GB
  • With replication (3x): ~1TB total

Inventory:

  • SKUs: 100M products
  • Inventory checks: 500M/day (1 per cart view)
  • Reservation writes: 150M/day

Best when:

  • High-value items where overselling has significant cost (electronics, luxury goods)
  • Limited inventory flash sales (concert tickets, limited editions)
  • Regulatory requirements for inventory accuracy

Architecture:

  • Strong consistency for all inventory operations
  • Synchronous inventory checks before cart add
  • Pessimistic locking during checkout

Trade-offs:

  • Zero overselling
  • Accurate inventory counts everywhere
  • Higher latency (lock contention)
  • Lower throughput under load
  • Checkout failures increase during traffic spikes

Real-world example: Ticketmaster uses strong consistency for seat inventory. During high-demand events, this leads to “waiting room” queuing to serialize access and prevent overselling reserved seating.

Best when:

  • Large inventory buffers make overselling rare
  • Customer experience (speed) more important than perfect accuracy
  • Compensation for overselling is acceptable (refund + coupon)

Architecture:

  • Eventually consistent inventory reads
  • Optimistic updates with conflict resolution at checkout
  • Accept occasional overselling, handle via backorder/compensation

Trade-offs:

  • Sub-millisecond cart operations
  • Handles extreme traffic spikes gracefully
  • Better user experience (no waiting)
  • Occasional overselling (typically <0.1%)
  • Requires robust compensation workflow

Real-world example: Amazon accepts minor overselling on most products. When it occurs, customers receive an apology email with a discount code and the option to wait for restock or cancel. The revenue from faster checkout far exceeds compensation costs.

FactorPath A: Consistency-FirstPath B: Availability-First
Inventory accuracy100%99.9%+
Cart add latency50-200ms<10ms
Peak throughput10K ops/sec100K+ ops/sec
Checkout failure rateHigher (locks timeout)Lower (optimistic)
Operational complexityLowerHigher (compensation flows)
Best forTickets, luxury, limitedGeneral retail, commodities

This article focuses on Path B (Availability-First) because it represents the architecture of most large-scale e-commerce systems (Amazon, Shopify, Walmart). Path A patterns are noted where inventory criticality requires them.

Async

Checkout Domain

Inventory Domain

Pricing Domain

Cart Domain

Gateway Layer

Client

Browser/Mobile

API Gateway

Auth Service

Cart Service

Redis Cart Cache

Cart Database

Pricing Service

Promotion Engine

Price Cache

Inventory Service

Reservation Cache

Inventory Database

Checkout Orchestrator

Payment Service

Order Service

Order Database

Message Queue

Cart Expiry Worker

Notification Worker

Manages cart lifecycle: creation, item management, persistence, and merge operations.

Responsibilities:

  • CRUD operations on cart items
  • Guest cart token generation and management
  • Cart merge on user authentication
  • Price/availability validation coordination
  • Cart expiration scheduling

Data Flow - Add to Cart:

CartDBRedisPricingServiceInventoryServiceCartServiceClientCartDBRedisPricingServiceInventoryServiceCartServiceClientPOST /cart/itemsCheck availabilityAvailable (qty: 50)Soft reserve (qty: 1, ttl: 5min)Reservation IDGet current price$29.99Update cart cachePersist cart (async)200 OK (cart updated)

Manages stock levels, reservations, and availability across warehouses.

Key Concepts:

  • Available For Sale (AFS): Physical stock minus hard reservations
  • Available For Reservation (AFR): AFS minus soft reservations
  • Soft Reservation: Temporary hold with TTL, automatically releases
  • Hard Reservation: Committed hold after payment, triggers fulfillment

Reservation State Machine:

Add to cart

TTL expires / Remove from cart

Payment confirmed

Pick ticket created

Shipment dispatched

Order cancelled

Available

SoftReserved

HardReserved

Allocated

Shipped

Coordinates the multi-step checkout process using the Saga pattern.

Saga Steps:

  1. Validate Cart: Confirm items still in stock at current prices
  2. Authorize Payment: Place hold on payment method
  3. Convert Reservations: Soft → Hard for all cart items
  4. Create Order: Generate order record
  5. Confirm Payment: Capture authorized amount
  6. Trigger Fulfillment: Send to warehouse

Compensation Actions (on failure):

  • Payment capture failed → Release hard reservations, void authorization
  • Reservation conversion failed → Void payment authorization
  • Order creation failed → Release reservations, void authorization

Evaluates pricing rules, promotions, and coupons in real-time.

Rule Evaluation Order:

  1. Base price (from catalog)
  2. Sale price (time-based overrides)
  3. Quantity discounts (buy 3, get 10% off)
  4. Coupon codes (user-applied)
  5. Cart-level promotions (free shipping over $50)
  6. Loyalty discounts (member pricing)

Conflict Resolution:

  • Exclusive promotions marked in rule metadata
  • Priority field determines evaluation order
  • “Best for customer” mode: apply combination yielding maximum discount
GET /api/v1/cart
Authorization: Bearer {token} | X-Guest-Token: {guest_token}

Response (200 OK):

{
"cart_id": "cart_abc123",
"user_id": "user_xyz789",
"items": [
{
"item_id": "item_001",
"product_id": "prod_12345",
"product_name": "Wireless Headphones",
"variant_id": "var_black_medium",
"quantity": 2,
"unit_price": 79.99,
"line_total": 159.98,
"image_url": "https://cdn.example.com/headphones.jpg",
"availability": {
"status": "in_stock",
24 collapsed lines
"quantity_available": 45,
"reservation_expires_at": "2024-01-15T10:35:00Z"
},
"applied_promotions": [
{
"promotion_id": "promo_winter_sale",
"name": "Winter Sale 20% Off",
"discount_amount": 31.99
}
]
}
],
"summary": {
"subtotal": 159.98,
"discount_total": 31.99,
"shipping_estimate": 0.0,
"tax_estimate": 10.24,
"total": 138.23
},
"applied_coupons": [],
"created_at": "2024-01-15T09:00:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"expires_at": "2024-02-14T09:00:00Z"
}

Design Decisions:

  • availability embedded per item: Frontend can show stock warnings without additional calls
  • reservation_expires_at exposed: Client can show countdown timer encouraging checkout
  • summary pre-calculated: Avoids client-side price calculation errors
  • Pagination not needed: Carts rarely exceed 50 items; full payload < 10KB
POST /api/v1/cart/items
Authorization: Bearer {token} | X-Guest-Token: {guest_token}
Content-Type: application/json
Idempotency-Key: {uuid}

Request:

{
"product_id": "prod_12345",
"variant_id": "var_black_medium",
"quantity": 2
}

Response (201 Created):

{
"item_id": "item_001",
"product_id": "prod_12345",
"quantity": 2,
12 collapsed lines
"unit_price": 79.99,
"line_total": 159.98,
"reservation": {
"reservation_id": "res_abc123",
"expires_at": "2024-01-15T10:35:00Z"
},
"cart_summary": {
"item_count": 2,
"subtotal": 159.98,
"total": 138.23
}
}

Error Responses:

StatusConditionBody
400Invalid product/variant ID{"error": "INVALID_PRODUCT", "message": "Product not found"}
409Insufficient inventory{"error": "INSUFFICIENT_STOCK", "available": 1, "requested": 2}
409Duplicate add (idempotency)Returns original response
429Rate limit exceeded{"error": "RATE_LIMITED", "retry_after": 60}

Rate Limits: 60 requests/minute per user (prevents cart bombing attacks)

PATCH /api/v1/cart/items/{item_id}

Request:

{
"quantity": 3
}

Behavior:

  • quantity: 0 removes the item
  • Validates against available inventory
  • Updates soft reservation (extends TTL if increasing, releases delta if decreasing)
POST /api/v1/cart/coupons

Request:

{
"code": "SAVE20"
}

Response (200 OK):

{
"coupon": {
"code": "SAVE20",
"description": "20% off your order",
"discount_type": "percentage",
"discount_value": 20,
"applied_discount": 27.99
},
"cart_summary": {
"subtotal": 159.98,
"discount_total": 59.98,
"total": 110.24
}
}

Error Responses:

StatusCondition
400Invalid/expired coupon
409Coupon not combinable with existing promotions
409Minimum order value not met
POST /api/v1/checkout
Authorization: Bearer {token}

Request:

{
"cart_id": "cart_abc123"
}

Response (201 Created):

{
"checkout_id": "checkout_xyz789",
"status": "pending",
"cart_snapshot": {
"items": [...],
"summary": {...}
5 collapsed lines
},
"required_steps": ["address", "payment", "review"],
"completed_steps": [],
"expires_at": "2024-01-15T11:00:00Z"
}

Design Decisions:

  • cart_snapshot captured at checkout init: Prices locked for checkout duration
  • expires_at enforced: 30-minute checkout session prevents indefinite reservation holds
  • Steps returned by server: Enables A/B testing checkout flows without client changes
PUT /api/v1/checkout/{checkout_id}/address

Request:

{
"shipping_address": {
"name": "John Doe",
"line1": "123 Main St",
"line2": "Apt 4B",
"city": "Seattle",
"state": "WA",
"postal_code": "98101",
"country": "US",
"phone": "+1-206-555-0123"
},
"billing_same_as_shipping": true
}

Response includes:

  • Validated/normalized address
  • Updated shipping options with real costs
  • Tax calculation based on destination
POST /api/v1/checkout/{checkout_id}/complete
Idempotency-Key: {uuid}

Request:

{
"payment_method_id": "pm_card_visa_4242",
"accept_terms": true
}

Response (201 Created):

{
"order_id": "order_abc123",
"status": "confirmed",
"confirmation_number": "AMZ-2024-ABC123",
"estimated_delivery": "2024-01-18",
"total_charged": 138.23,
"payment": {
4 collapsed lines
"method": "Visa ending in 4242",
"transaction_id": "txn_xyz789"
}
}

Idempotency Behavior:

  • Same Idempotency-Key within 24 hours returns cached response
  • Prevents duplicate charges on network retries or user double-clicks

Primary Store: PostgreSQL (ACID guarantees, complex merge queries)

5 collapsed lines
-- Cart table
CREATE TABLE carts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
guest_token VARCHAR(64) UNIQUE,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ,
merged_into_cart_id UUID REFERENCES carts(id),
CONSTRAINT user_or_guest CHECK (
(user_id IS NOT NULL AND guest_token IS NULL) OR
(user_id IS NULL AND guest_token IS NOT NULL)
)
);
-- Cart items table
CREATE TABLE cart_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
cart_id UUID NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
product_id UUID NOT NULL,
variant_id UUID NOT NULL,
quantity INT NOT NULL CHECK (quantity > 0),
15 collapsed lines
unit_price_at_add DECIMAL(10,2) NOT NULL,
reservation_id UUID,
added_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (cart_id, product_id, variant_id)
);
-- Indexes for common access patterns
CREATE INDEX idx_carts_user ON carts(user_id) WHERE user_id IS NOT NULL;
CREATE INDEX idx_carts_guest ON carts(guest_token) WHERE guest_token IS NOT NULL;
CREATE INDEX idx_carts_expires ON carts(expires_at) WHERE status = 'active';
CREATE INDEX idx_cart_items_cart ON cart_items(cart_id);
CREATE INDEX idx_cart_items_reservation ON cart_items(reservation_id)
WHERE reservation_id IS NOT NULL;

Design Decisions:

  • user_id vs guest_token mutual exclusion: Clean separation of authenticated vs guest carts
  • unit_price_at_add: Audit trail for price changes between add and checkout
  • reservation_id nullable: Not all cart items require reservation (digital goods)
  • Soft delete via merged_into_cart_id: Preserves guest cart history for analytics
# Cart metadata (Hash)
HSET cart:{cart_id}
user_id "user_xyz789"
item_count 3
subtotal 259.97
updated_at 1705312200
# Cart items (Hash - one per item)
HSET cart:{cart_id}:item:{item_id}
product_id "prod_12345"
variant_id "var_black_medium"
quantity 2
unit_price 79.99
reservation_id "res_abc123"
reservation_expires 1705312500
# Guest token to cart mapping
SET guest:{guest_token} cart_abc123 EX 2592000 # 30 days
# Cart expiration sorted set (for cleanup workers)
ZADD cart_expirations 1705312500 cart_abc123

TTL Strategy:

  • Cart metadata: 30 days (matches business retention policy)
  • Reservation entries: 5 minutes (align with soft reservation TTL)
  • Guest token mapping: 30 days

Primary Store: PostgreSQL with read replicas

3 collapsed lines
-- Inventory by location
CREATE TABLE inventory_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID NOT NULL,
variant_id UUID NOT NULL,
location_id UUID NOT NULL,
quantity_on_hand INT NOT NULL DEFAULT 0,
quantity_reserved INT NOT NULL DEFAULT 0,
quantity_available INT GENERATED ALWAYS AS
(quantity_on_hand - quantity_reserved) STORED,
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE (product_id, variant_id, location_id),
CHECK (quantity_reserved <= quantity_on_hand)
);
-- Reservations table
CREATE TABLE inventory_reservations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
12 collapsed lines
inventory_entry_id UUID NOT NULL REFERENCES inventory_entries(id),
cart_id UUID NOT NULL,
quantity INT NOT NULL,
type VARCHAR(10) NOT NULL CHECK (type IN ('soft', 'hard')),
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ, -- NULL for hard reservations
order_id UUID, -- Set when converted to hard reservation
INDEX idx_reservations_entry (inventory_entry_id),
INDEX idx_reservations_cart (cart_id),
INDEX idx_reservations_expires (expires_at) WHERE type = 'soft'
);

Consistency Approach:

  • quantity_available as generated column: Always consistent with underlying values
  • Reservation updates use SELECT FOR UPDATE: Prevents race conditions
  • Read replicas used for availability display (eventual consistency acceptable)
# Soft reservation with automatic expiry
SET reservation:{res_id}
'{"inventory_entry_id":"inv_123","cart_id":"cart_abc","quantity":2}'
EX 300 # 5 minutes
# Fast lookup: cart → reservations
SADD cart_reservations:{cart_id} res_001 res_002
EXPIRE cart_reservations:{cart_id} 300
# Fast lookup: inventory → reservations (for availability calculation)
SADD inventory_reservations:{inventory_entry_id} res_001 res_002
EXPIRE inventory_reservations:{inventory_entry_id} 300

Why Redis for reservations:

  • Automatic TTL expiration handles cleanup without background jobs
  • Sub-millisecond lookups for availability checks
  • SADD operations for atomic reservation tracking
Data TypeStoreRationale
Cart (persistent)PostgreSQLACID for merge operations, complex queries
Cart (cache)Redis ClusterSub-ms reads, automatic expiration
InventoryPostgreSQL + replicasStrong consistency writes, scaled reads
ReservationsRedis + PostgreSQLRedis for speed, PG for durability
OrdersPostgreSQLACID required for financial records
Price rulesPostgreSQL + cacheComplex queries, Redis for hot paths

Cart merge occurs when a guest user authenticates. The system must combine items from both carts while handling conflicts.

No

Yes

No

Yes

Sum quantities

Keep higher

Keep user

Yes

No

User logs in with guest_token

User has existing cart?

Convert guest cart to user cart

Load both carts

For each guest cart item

Item exists in user cart?

Add item to user cart

Merge strategy

Add quantities, cap at max

Take max quantity

Ignore guest item

More items?

Update reservations

Mark guest cart as merged

Return merged cart

15 collapsed lines
interface CartItem {
productId: string
variantId: string
quantity: number
reservationId?: string
}
interface MergeResult {
mergedCart: Cart
addedItems: CartItem[]
updatedItems: Array<{ item: CartItem; previousQty: number }>
conflicts: Array<{ guestItem: CartItem; reason: string }>
}
async function mergeGuestCart(
userId: string,
guestToken: string,
strategy: "sum" | "max" | "keep_user" = "sum",
): Promise<MergeResult> {
return await db.transaction(async (tx) => {
// Load both carts with row-level locks
const [userCart, guestCart] = await Promise.all([
tx.query("SELECT * FROM carts WHERE user_id = $1 FOR UPDATE", [userId]),
tx.query("SELECT * FROM carts WHERE guest_token = $1 FOR UPDATE", [guestToken]),
])
if (!guestCart) {
return { mergedCart: userCart, addedItems: [], updatedItems: [], conflicts: [] }
}
const result: MergeResult = {
mergedCart: userCart || (await createUserCart(tx, userId)),
addedItems: [],
updatedItems: [],
conflicts: [],
}
for (const guestItem of guestCart.items) {
const existingItem = result.mergedCart.items.find(
(i) => i.productId === guestItem.productId && i.variantId === guestItem.variantId,
)
if (!existingItem) {
// Transfer item to user cart
21 collapsed lines
await transferItem(tx, guestItem, result.mergedCart.id)
result.addedItems.push(guestItem)
} else {
// Handle conflict based on strategy
const newQty = resolveQuantity(existingItem.quantity, guestItem.quantity, strategy)
const maxAllowed = await getMaxQuantity(guestItem.productId, guestItem.variantId)
if (newQty > maxAllowed) {
result.conflicts.push({
guestItem,
reason: `Quantity capped at ${maxAllowed} (max per order)`,
})
}
if (newQty !== existingItem.quantity) {
await updateItemQuantity(tx, existingItem.id, Math.min(newQty, maxAllowed))
result.updatedItems.push({ item: existingItem, previousQty: existingItem.quantity })
}
// Release guest item's reservation (will be replaced by user cart's)
if (guestItem.reservationId) {
await releaseReservation(guestItem.reservationId)
}
}
}
// Mark guest cart as merged
await tx.query("UPDATE carts SET status = $1, merged_into_cart_id = $2 WHERE id = $3", [
"merged",
result.mergedCart.id,
guestCart.id,
])
return result
})
}
function resolveQuantity(userQty: number, guestQty: number, strategy: string): number {
switch (strategy) {
case "sum":
return userQty + guestQty
case "max":
return Math.max(userQty, guestQty)
case "keep_user":
return userQty
}
}
ScenarioHandling
Guest item now out of stockAdd to cart with unavailable flag; notify user
Price changed since guest addUse current price; show price change notice
Guest item discontinuedAdd to “saved items” instead; notify user
Combined quantity exceeds limitCap at limit; show conflict message
Guest cart has applied couponValidate coupon for user; may not transfer (user-specific coupons)

The checkout process spans multiple services that must coordinate atomically despite having independent databases.

OrderServicePaymentServiceInventoryServiceCartServiceOrchestratorClientOrderServicePaymentServiceInventoryServiceCartServiceOrchestratorClientalt[Conversion fails]alt[Order creation fails]alt[Capture fails]POST /checkout/completeValidate cart (prices, availability)Cart validAuthorize paymentAuthorization IDConvert soft→hard reservationsInsufficient stockVoid authorization409 Conflict (stock unavailable)Hard reservations confirmedCreate orderErrorRelease hard reservationsVoid authorization500 Error (retry)Order IDCapture paymentCapture failedMark order as payment_failedRelease hard reservations402 Payment RequiredCapture confirmedUpdate order status to confirmedClear cart201 Created (order confirmed)
10 collapsed lines
enum CheckoutState {
INITIATED = "initiated",
CART_VALIDATED = "cart_validated",
PAYMENT_AUTHORIZED = "payment_authorized",
INVENTORY_RESERVED = "inventory_reserved",
ORDER_CREATED = "order_created",
PAYMENT_CAPTURED = "payment_captured",
COMPLETED = "completed",
COMPENSATION_REQUIRED = "compensation_required",
FAILED = "failed",
}
interface CheckoutSaga {
id: string
cartId: string
state: CheckoutState
authorizationId?: string
orderId?: string
failedStep?: string
compensationSteps: string[]
createdAt: Date
updatedAt: Date
}
async function executeCheckoutSaga(checkoutId: string): Promise<Order> {
const saga = await loadSaga(checkoutId)
try {
// Each step is idempotent and checks current state before executing
if (saga.state === CheckoutState.INITIATED) {
await validateCart(saga)
await transitionState(saga, CheckoutState.CART_VALIDATED)
}
if (saga.state === CheckoutState.CART_VALIDATED) {
saga.authorizationId = await authorizePayment(saga)
await transitionState(saga, CheckoutState.PAYMENT_AUTHORIZED)
}
21 collapsed lines
if (saga.state === CheckoutState.PAYMENT_AUTHORIZED) {
await convertReservations(saga)
await transitionState(saga, CheckoutState.INVENTORY_RESERVED)
}
if (saga.state === CheckoutState.INVENTORY_RESERVED) {
saga.orderId = await createOrder(saga)
await transitionState(saga, CheckoutState.ORDER_CREATED)
}
if (saga.state === CheckoutState.ORDER_CREATED) {
await capturePayment(saga)
await transitionState(saga, CheckoutState.PAYMENT_CAPTURED)
}
if (saga.state === CheckoutState.PAYMENT_CAPTURED) {
await clearCart(saga)
await transitionState(saga, CheckoutState.COMPLETED)
}
return await loadOrder(saga.orderId)
} catch (error) {
saga.failedStep = saga.state
await transitionState(saga, CheckoutState.COMPENSATION_REQUIRED)
await executeCompensation(saga)
throw error
}
}
async function executeCompensation(saga: CheckoutSaga): Promise<void> {
// Compensate in reverse order of completed steps
if (saga.orderId && saga.state !== CheckoutState.PAYMENT_CAPTURED) {
await markOrderFailed(saga.orderId)
}
if (saga.state >= CheckoutState.INVENTORY_RESERVED) {
await releaseHardReservations(saga.cartId)
}
if (saga.authorizationId) {
await voidAuthorization(saga.authorizationId)
}
await transitionState(saga, CheckoutState.FAILED)
}
8 collapsed lines
interface IdempotencyRecord {
key: string
requestHash: string
response: any
statusCode: number
createdAt: Date
expiresAt: Date
}
async function withIdempotency<T>(
key: string,
request: any,
handler: () => Promise<T>,
): Promise<{ result: T; statusCode: number; cached: boolean }> {
const requestHash = hashRequest(request)
// Check for existing response
const existing = await redis.get(`idempotency:${key}`)
if (existing) {
const record: IdempotencyRecord = JSON.parse(existing)
if (record.requestHash === requestHash) {
return { result: record.response, statusCode: record.statusCode, cached: true }
}
// Same key, different request = error
throw new ConflictError("Idempotency key reused with different request")
}
// Lock the key during processing
const lockAcquired = await redis.set(
16 collapsed lines
`idempotency:${key}`,
JSON.stringify({ requestHash, status: "processing" }),
"NX",
"EX",
300, // 5 minute lock
)
if (!lockAcquired) {
// Another request is processing with same key
throw new ConflictError("Request already in progress")
}
try {
const result = await handler()
const record: IdempotencyRecord = {
key,
requestHash,
response: result,
statusCode: 201,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
}
await redis.set(`idempotency:${key}`, JSON.stringify(record), "EX", 86400)
return { result, statusCode: 201, cached: false }
} catch (error) {
await redis.del(`idempotency:${key}`)
throw error
}
}

Naive approach:

// ❌ Array-based: O(n) lookups for quantity updates
interface Cart {
items: CartItem[]
}

Optimized approach:

// ✅ Normalized: O(1) lookups, efficient updates
interface CartState {
items: Record<string, CartItem> // itemId → CartItem
itemOrder: string[] // Maintains display order
summary: CartSummary
appliedCoupons: Coupon[]
reservationTimers: Record<string, number> // itemId → expiresAt
}

Why normalized:

  • Quantity update: Update single object, no array scan
  • Remove item: Delete from items, filter itemOrder
  • Reorder: Modify itemOrder only
  • React renders: Reference equality checks work correctly
10 collapsed lines
import { useMutation, useQueryClient } from "@tanstack/react-query"
function useAddToCart() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: addItemToCart,
onMutate: async (newItem) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ["cart"] })
// Snapshot previous state
const previousCart = queryClient.getQueryData(["cart"])
// Optimistically update
queryClient.setQueryData(["cart"], (old: CartState) => ({
...old,
items: {
...old.items,
[newItem.itemId]: {
...newItem,
status: "pending", // Visual indicator
},
},
itemOrder: [...old.itemOrder, newItem.itemId],
summary: recalculateSummary(old, newItem),
}))
return { previousCart }
},
onError: (err, newItem, context) => {
// Rollback on error
queryClient.setQueryData(["cart"], context.previousCart)
showToast({
type: "error",
message: err.code === "INSUFFICIENT_STOCK" ? `Only ${err.available} available` : "Failed to add item",
})
21 collapsed lines
},
onSuccess: (data, newItem) => {
// Update with server response (includes reservation info)
queryClient.setQueryData(["cart"], (old: CartState) => ({
...old,
items: {
...old.items,
[newItem.itemId]: {
...data.item,
status: "confirmed",
},
},
summary: data.cartSummary,
}))
},
onSettled: () => {
// Refetch to ensure consistency
queryClient.invalidateQueries({ queryKey: ["cart"] })
},
})
}
5 collapsed lines
function useReservationTimer(expiresAt: string | null) {
const [timeLeft, setTimeLeft] = useState<number | null>(null);
const [isExpired, setIsExpired] = useState(false);
useEffect(() => {
if (!expiresAt) return;
const updateTimer = () => {
const remaining = new Date(expiresAt).getTime() - Date.now();
if (remaining <= 0) {
setIsExpired(true);
setTimeLeft(0);
} else {
setTimeLeft(Math.ceil(remaining / 1000));
}
};
updateTimer();
const interval = setInterval(updateTimer, 1000);
return () => clearInterval(interval);
}, [expiresAt]);
return { timeLeft, isExpired };
}
11 collapsed lines
// Usage in component
function CartItem({ item }: { item: CartItemData }) {
const { timeLeft, isExpired } = useReservationTimer(item.reservationExpiresAt);
return (
<div className={isExpired ? 'item-expired' : ''}>
{/* ... item display ... */}
{timeLeft !== null && timeLeft < 300 && (
<div className="reservation-warning">
Reserved for {formatTime(timeLeft)} - complete checkout soon
</div>
)}
{isExpired && (
<div className="reservation-expired">
Reservation expired - item may become unavailable
</div>
)}
</div>
);
}
10 collapsed lines
function useCartPriceSync(cartId: string) {
const queryClient = useQueryClient()
useEffect(() => {
const ws = new WebSocket(`wss://api.example.com/cart/${cartId}/updates`)
ws.onmessage = (event) => {
const update = JSON.parse(event.data)
switch (update.type) {
case "price_change":
queryClient.setQueryData(["cart"], (old: CartState) => {
const item = old.items[update.itemId]
if (!item) return old
const priceDiff = update.newPrice - item.unitPrice
return {
...old,
items: {
...old.items,
[update.itemId]: {
...item,
unitPrice: update.newPrice,
lineTotal: update.newPrice * item.quantity,
priceChanged: priceDiff !== 0,
priceDiff,
},
},
summary: recalculateSummary(old, update),
16 collapsed lines
}
})
break
case "item_unavailable":
queryClient.setQueryData(["cart"], (old: CartState) => ({
...old,
items: {
...old.items,
[update.itemId]: {
...old.items[update.itemId],
available: false,
availableQuantity: update.availableQuantity,
},
},
}))
showToast({
type: "warning",
message: `${old.items[update.itemId].name} is now out of stock`,
})
break
case "reservation_expired":
queryClient.invalidateQueries({ queryKey: ["cart"] })
break
}
}
return () => ws.close()
}, [cartId, queryClient])
}
ComponentConceptRequirements
Cart ServiceStateless API serversAuto-scaling, health checks
Checkout OrchestratorStateful workflow engineDurable execution, retry support
Background WorkersJob processorsAt-least-once delivery, idempotency
DataConceptRequirements
Cart (hot)In-memory cacheSub-ms reads, TTL support, clustering
Cart (persistent)Relational DBACID, complex queries, replication
InventoryRelational DBStrong consistency, row-level locking
ReservationsKV store with TTLAutomatic expiration, high throughput
OrdersRelational DBACID, audit trail
Use CaseConceptRequirements
Cart eventsMessage queueAt-least-once, ordering per cart
Inventory updatesEvent streamFan-out to multiple consumers
Abandoned cartDelayed queueScheduled delivery

Async

Data

Compute - VPC

Edge

ECS Cluster

CloudFront

AWS WAF

Application Load Balancer

Cart Service

Fargate

Checkout Service

Fargate

Inventory Service

Fargate

Step Functions

Checkout Saga

ElastiCache

Redis Cluster

RDS PostgreSQL

Multi-AZ

DynamoDB

Reservations

SQS

Cart Events

EventBridge

Scheduler

Lambda

Workers

ComponentAWS ServiceConfiguration
Cart APIECS Fargate2-50 tasks, auto-scaling on CPU
Cart cacheElastiCache Redisr6g.large, cluster mode, 3 shards
Cart DBRDS PostgreSQLdb.r6g.xlarge, Multi-AZ, 2 read replicas
ReservationsDynamoDBOn-demand, TTL enabled
Checkout sagaStep FunctionsExpress workflow (30 min max)
Event busSQS + EventBridgeStandard queue, 14-day retention
Background workersLambda1024MB, 15-min timeout
CDNCloudFrontPrice class 100 (US/EU)
WAFAWS WAFRate limiting, SQL injection protection

For high availability during peak events:

us-west-2 (Secondary)

us-east-1 (Primary)

Async Replication

ALB

ECS Cluster

RDS Primary

ElastiCache

ALB

ECS Cluster

RDS Replica

ElastiCache

Route 53

Latency Routing

Global Accelerator

Failover Strategy:

  • Route 53 health checks detect primary failure
  • Global Accelerator reroutes traffic to secondary
  • RDS replica promoted to primary (RPO: ~1 minute)
  • Redis cache rebuilt from database (acceptable for carts)
Managed ServiceSelf-Hosted OptionWhen to Self-Host
ElastiCacheRedis Cluster on EC2Specific modules (RedisGraph, RedisJSON)
RDS PostgreSQLPostgreSQL on EC2Cost at scale, specific extensions
DynamoDBScyllaDB / CassandraMulti-cloud, cost optimization
Step FunctionsTemporal.ioComplex workflows, long-running sagas
SQSRabbitMQ / Redis StreamsSpecific routing needs

This shopping cart design prioritizes availability and user experience over perfect consistency, accepting that:

  1. Eventual consistency for display is acceptable when strong consistency is enforced at checkout
  2. Soft reservations with expiry prevent inventory lock-up while providing reasonable purchase assurance
  3. Saga orchestration provides reliable distributed transactions with clear compensation paths
  4. Multi-tier caching delivers sub-millisecond cart reads while maintaining durability

Key tradeoffs accepted:

  • Occasional checkout failures when reservations expire (mitigated by clear countdown UX)
  • Rare overselling on flash sales (handled via backorder/compensation)
  • Higher operational complexity from distributed architecture (justified by scale requirements)

What this design does NOT address:

  • Multi-currency pricing (requires additional currency service)
  • Subscription/recurring purchases (different cart model)
  • B2B bulk ordering (different quantity/pricing rules)
  • Marketplace multi-seller carts (complex checkout splitting)
  • Distributed systems fundamentals (CAP theorem, eventual consistency)
  • Database concepts (ACID, sharding, replication)
  • API design principles (REST, idempotency)
  • Basic understanding of payment processing flows
TermDefinition
AFS (Available For Sale)Physical inventory minus hard reservations
AFR (Available For Reservation)AFS minus soft reservations
Soft ReservationTemporary inventory hold with automatic expiry (typically 5 minutes)
Hard ReservationCommitted inventory allocation after payment confirmation
SagaPattern for distributed transactions using compensating actions
Idempotency KeyClient-generated UUID ensuring duplicate requests return same response
Cart MergeProcess of combining guest cart with authenticated user’s cart
  • Two-tier storage (Redis + PostgreSQL) balances latency and durability for cart operations
  • Soft/hard reservation model prevents inventory lock-up while providing checkout assurance
  • Saga orchestration with compensation ensures reliable multi-service checkout
  • Eventually consistent inventory reads with strongly consistent checkout enables scale
  • Normalized frontend state with optimistic updates delivers responsive UX
  • Multi-region deployment with failover provides 99.99% availability target

Read more