Loading content...
The shopping cart is arguably the most complex stateful component in an e-commerce platform. Unlike catalog data (read-mostly) or orders (write-once), carts are in constant flux—items added, quantities changed, removed, saved for later, and eventually converted to orders or abandoned.
A well-designed cart system must handle:
Amazon famously optimized their cart to be one of the fastest operations on the site because cart latency directly impacts conversion. The 'Add to Cart' button must respond in under 100ms, even while performing inventory checks, price validation, and cross-device sync.
This page explores the complete architecture of a shopping cart system designed for this scale and complexity.
By the end of this page, you will understand how to design a cart storage strategy that balances consistency and availability; implement cart merging logic for guest-to-authenticated user flows; handle inventory race conditions without degrading UX; and optimize for the metrics that matter: cart conversion and abandonment rates.
A shopping cart appears simple on the surface—a list of items with quantities—but production cart data models are considerably more complex. The cart must represent:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
// Root Cart Entityinterface Cart { id: string; // UUID for the cart userId?: string; // Null for anonymous carts sessionId: string; // Browser/app session identifier // Cart items items: CartItem[]; savedItems: SavedItem[]; // "Save for Later" items // Computed totals (cached, recalculated on changes) subtotal: Money; estimatedShipping: Money; estimatedTax: Money; discount: Money; total: Money; // Promotions appliedPromoCodes: PromoCode[]; autoAppliedPromotions: Promotion[]; // System-applied deals // Metadata createdAt: Timestamp; updatedAt: Timestamp; lastAccessedAt: Timestamp; // For abandonment tracking expiresAt: Timestamp; // TTL for anonymous carts // Source tracking createdFrom: 'web' | 'mobile_app' | 'api'; lastModifiedFrom: 'web' | 'mobile_app' | 'api'; // State flags validationState: CartValidationState; requiresRevalidation: boolean; // Set when prices/stock change} // Individual cart iteminterface CartItem { id: string; // Unique within cart productId: string; variantId: string; // Snapshot of product data at add time (for comparison) productSnapshot: { title: string; brand: string; imageUrl: string; priceAtAdd: Money; // Price when added }; // Current product data (updated on cart access) currentPrice: Money; // May differ from priceAtAdd quantity: number; maxQuantity: number; // Per-item purchase limits // Availability stockStatus: 'in_stock' | 'low_stock' | 'out_of_stock' | 'preorder'; availableQuantity: number; // Actual available quantity // Fulfillment options for this item fulfillmentOptions: FulfillmentOption[]; selectedFulfillment?: string; // ID of chosen option // Item-level promotions appliedDiscounts: ItemDiscount[]; // Customization giftWrap: boolean; giftMessage?: string; // Timestamps addedAt: Timestamp; updatedAt: Timestamp;} // Cart validation state (computed on access)interface CartValidationState { isValid: boolean; lastValidated: Timestamp; warnings: CartWarning[]; // Non-blocking issues errors: CartError[]; // Blocking issues priceChangedItems: string[]; // Item IDs with price changes unavailableItems: string[]; // Item IDs now out of stock quantityAdjustedItems: string[]; // Items where quantity was reduced} interface CartWarning { type: 'price_increased' | 'price_decreased' | 'low_stock' | 'limited_quantity'; itemId: string; message: string; data: Record<string, any>;} interface CartError { type: 'out_of_stock' | 'item_discontinued' | 'variant_unavailable'; itemId: string; message: string; suggestedAction: 'remove' | 'replace' | 'waitlist';}Storing a 'snapshot' of product data at add time enables detecting changes. This lets us show 'Price dropped!' or 'Price increased since you added' messages—powerful UX that builds trust and drives conversion.
Cart storage requires a different approach than catalog storage. Carts are:
These characteristics make key-value stores the natural choice, with Redis being the most common solution.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
┌──────────────────────────────────────────────────────────────────────────────┐│ CART STORAGE ARCHITECTURE │├──────────────────────────────────────────────────────────────────────────────┤│ ││ ┌─────────────────────────────────────────────────────────────────────────┐ ││ │ PRIMARY: REDIS CLUSTER │ ││ │ │ ││ │ • 99%+ of cart operations hit Redis only │ ││ │ • Sub-millisecond read/write latency │ ││ │ • Native TTL for cart expiration │ ││ │ • Cluster mode for horizontal scaling │ ││ │ │ ││ │ Storage Pattern: │ ││ │ ┌──────────────────────────────────────────────────────────────────┐ │ ││ │ │ Key: cart:{cartId} │ │ ││ │ │ Value: JSON-serialized Cart object │ │ ││ │ │ TTL: 30 days for authenticated, 7 days for anonymous │ │ ││ │ └──────────────────────────────────────────────────────────────────┘ │ ││ │ ┌──────────────────────────────────────────────────────────────────┐ │ ││ │ │ Key: user_cart:{userId} │ │ ││ │ │ Value: cartId (lookup index) │ │ ││ │ │ TTL: Never expires for authenticated users │ │ ││ │ └──────────────────────────────────────────────────────────────────┘ │ ││ │ ┌──────────────────────────────────────────────────────────────────┐ │ ││ │ │ Key: session_cart:{sessionId} │ │ ││ │ │ Value: cartId (for anonymous session tracking) │ │ ││ │ │ TTL: 7 days │ │ ││ │ └──────────────────────────────────────────────────────────────────┘ │ ││ └─────────────────────────────────────────────────────────────────────────┘ ││ ││ │ Async backup ││ ▼ ││ ││ ┌─────────────────────────────────────────────────────────────────────────┐ ││ │ BACKUP: DYNAMODB / PostgreSQL │ ││ │ │ ││ │ • Durable storage for high-value carts │ ││ │ • Recovery source if Redis data lost │ ││ │ • Analytics and reporting queries │ ││ │ • Long-term cart abandonment analysis │ ││ │ │ ││ │ Sync Strategy: │ ││ │ • Carts with value > $100 sync immediately │ ││ │ • Other carts sync every 5 minutes via batch job │ ││ │ • Cart converted to order? Archive immediately │ ││ │ │ ││ └─────────────────────────────────────────────────────────────────────────┘ ││ │└──────────────────────────────────────────────────────────────────────────────┘123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
class CartRepository { private redis: RedisCluster; private backup: CartBackupStore; // Get cart with multiple lookup strategies async getCart(identifier: CartIdentifier): Promise<Cart | null> { let cartId: string | null = null; // Try to find cart ID from identifier if (identifier.cartId) { cartId = identifier.cartId; } else if (identifier.userId) { cartId = await this.redis.get(`user_cart:${identifier.userId}`); } else if (identifier.sessionId) { cartId = await this.redis.get(`session_cart:${identifier.sessionId}`); } if (!cartId) return null; // Get cart data const cartData = await this.redis.get(`cart:${cartId}`); if (!cartData) { // Try backup store (Redis might have evicted) return this.backup.getCart(cartId); } const cart = JSON.parse(cartData) as Cart; // Update last accessed timestamp (don't await - fire and forget) this.touchCart(cart.id); return cart; } // Add item with optimistic locking async addItem(cartId: string, item: AddItemRequest): Promise<CartUpdateResult> { // Use Redis transaction for atomic update const result = await this.redis.watch(`cart:${cartId}`); try { const cartData = await this.redis.get(`cart:${cartId}`); const cart = JSON.parse(cartData) as Cart; // Check if item already exists (same product + variant) const existingIndex = cart.items.findIndex( i => i.productId === item.productId && i.variantId === item.variantId ); if (existingIndex >= 0) { // Increment quantity instead of adding new item cart.items[existingIndex].quantity += item.quantity; cart.items[existingIndex].updatedAt = Date.now(); } else { // Add new item with product snapshot const productData = await this.catalogService.getProduct( item.productId, item.variantId ); cart.items.push({ id: generateUUID(), productId: item.productId, variantId: item.variantId, productSnapshot: { title: productData.title, brand: productData.brand, imageUrl: productData.primaryImage, priceAtAdd: productData.currentPrice, }, currentPrice: productData.currentPrice, quantity: item.quantity, maxQuantity: productData.maxPurchaseQuantity, stockStatus: productData.stockStatus, availableQuantity: productData.availableQuantity, fulfillmentOptions: productData.fulfillmentOptions, appliedDiscounts: [], giftWrap: false, addedAt: Date.now(), updatedAt: Date.now(), }); } // Recalculate totals this.recalculateTotals(cart); cart.updatedAt = Date.now(); cart.requiresRevalidation = false; // Commit transaction const pipeline = this.redis.multi(); pipeline.set(`cart:${cartId}`, JSON.stringify(cart)); pipeline.expire(`cart:${cartId}`, 30 * 24 * 60 * 60); // Extend TTL await pipeline.exec(); // Async backup for high-value carts if (cart.total.amount > 10000) { // > $100 this.backup.saveCart(cart).catch(err => console.error('Cart backup failed:', err) ); } return { success: true, cart, warnings: [] }; } catch (error) { if (error.message === 'WATCH_ERROR') { // Concurrent modification - retry return this.addItem(cartId, item); } throw error; } finally { await this.redis.unwatch(); } } // Atomic quantity update async updateQuantity( cartId: string, itemId: string, newQuantity: number ): Promise<CartUpdateResult> { // Use Lua script for atomic increment with validation const luaScript = ` local cart = cjson.decode(redis.call('GET', KEYS[1])) for i, item in ipairs(cart.items) do if item.id == ARGV[1] then local newQty = tonumber(ARGV[2]) if newQty <= 0 then table.remove(cart.items, i) elseif newQty <= item.maxQuantity then item.quantity = newQty item.updatedAt = tonumber(ARGV[3]) else return cjson.encode({error = 'EXCEEDS_MAX_QUANTITY'}) end cart.updatedAt = tonumber(ARGV[3]) redis.call('SET', KEYS[1], cjson.encode(cart)) return cjson.encode({success = true, cart = cart}) end end return cjson.encode({error = 'ITEM_NOT_FOUND'}) `; const result = await this.redis.eval( luaScript, 1, `cart:${cartId}`, itemId, newQuantity.toString(), Date.now().toString() ); return JSON.parse(result); }}Redis, even with AOF persistence, can lose recent writes on failure. For carts with significant value, async backup to DynamoDB/PostgreSQL is essential. The trade-off: Redis gives us speed, backup gives us durability. Design for 'at most a few seconds of cart changes lost' as acceptable.
One of the most nuanced cart operations is merging a guest cart with an authenticated user's existing cart when they log in. This operation must:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
interface CartMergeStrategy { // What to do when same product+variant exists in both carts duplicateItemStrategy: 'keep_guest' | 'keep_user' | 'sum_quantities' | 'max_quantity'; // What to do with guest cart after merge guestCartDisposition: 'delete' | 'archive' | 'keep_copy'; // Whether to carry forward guest cart promotions preserveGuestPromotions: boolean;} class CartMergeService { private defaultStrategy: CartMergeStrategy = { duplicateItemStrategy: 'sum_quantities', guestCartDisposition: 'archive', preserveGuestPromotions: true, }; async mergeGuestCart( userId: string, guestSessionId: string, options?: Partial<CartMergeStrategy> ): Promise<CartMergeResult> { const strategy = { ...this.defaultStrategy, ...options }; // Get both carts const [userCart, guestCart] = await Promise.all([ this.cartRepo.getCartByUserId(userId), this.cartRepo.getCartBySession(guestSessionId), ]); // No guest cart? Nothing to merge if (!guestCart || guestCart.items.length === 0) { return { merged: false, reason: 'no_guest_cart', resultCart: userCart }; } // No user cart? Adopt guest cart if (!userCart) { const adoptedCart = await this.adoptGuestCart(guestCart, userId); return { merged: true, action: 'adopted_guest', resultCart: adoptedCart }; } // Both carts exist - perform merge const mergedCart = await this.performMerge(userCart, guestCart, strategy); // Handle guest cart disposition await this.handleGuestCartDisposition(guestCart, strategy); // Track merge for analytics await this.analytics.trackCartMerge({ userId, guestSessionId, guestItemCount: guestCart.items.length, userItemCount: userCart.items.length, mergedItemCount: mergedCart.items.length, strategy: strategy.duplicateItemStrategy, }); return { merged: true, action: 'merged', resultCart: mergedCart, mergeReport: this.generateMergeReport(userCart, guestCart, mergedCart), }; } private async performMerge( userCart: Cart, guestCart: Cart, strategy: CartMergeStrategy ): Promise<Cart> { // Start with user cart as base const mergedCart = JSON.parse(JSON.stringify(userCart)) as Cart; const mergedItems = new Map<string, CartItem>(); // Index existing user items for (const item of mergedCart.items) { const key = `${item.productId}:${item.variantId}`; mergedItems.set(key, item); } // Process guest items for (const guestItem of guestCart.items) { const key = `${guestItem.productId}:${guestItem.variantId}`; const existingItem = mergedItems.get(key); if (existingItem) { // Handle duplicate switch (strategy.duplicateItemStrategy) { case 'keep_guest': mergedItems.set(key, { ...guestItem, id: existingItem.id }); break; case 'keep_user': // No change needed break; case 'sum_quantities': existingItem.quantity = Math.min( existingItem.quantity + guestItem.quantity, existingItem.maxQuantity ); // Keep the better (lower) price snapshot if (guestItem.productSnapshot.priceAtAdd.amount < existingItem.productSnapshot.priceAtAdd.amount) { existingItem.productSnapshot = guestItem.productSnapshot; } break; case 'max_quantity': existingItem.quantity = Math.max( existingItem.quantity, guestItem.quantity ); break; } } else { // New item from guest cart - add it mergedItems.set(key, { ...guestItem, id: generateUUID() // New ID for merged cart }); } } mergedCart.items = Array.from(mergedItems.values()); // Handle promotions if (strategy.preserveGuestPromotions) { const validGuestPromos = await this.validatePromotions( guestCart.appliedPromoCodes, mergedCart ); mergedCart.appliedPromoCodes = [ ...userCart.appliedPromoCodes, ...validGuestPromos.filter(p => !userCart.appliedPromoCodes.some(up => up.code === p.code) ) ]; } // Recalculate totals await this.recalculateTotals(mergedCart); mergedCart.updatedAt = Date.now(); // Save merged cart await this.cartRepo.saveCart(mergedCart); return mergedCart; } private async adoptGuestCart(guestCart: Cart, userId: string): Promise<Cart> { // Simply associate the guest cart with the user guestCart.userId = userId; guestCart.updatedAt = Date.now(); // Create user->cart mapping await this.redis.set(`user_cart:${userId}`, guestCart.id); await this.cartRepo.saveCart(guestCart); return guestCart; }}The cart must continuously validate that items remain available. This creates a fundamental tension:
The solution is a layered validation approach with different levels of checking based on context.
| Context | Validation Level | Latency Impact | Accuracy |
|---|---|---|---|
| Add to Cart | Snapshot check (cached inventory) | <5ms | ~99% accurate |
| View Cart | Async validation (background) | 0ms visible | Updated within 2s |
| Mini-cart Hover | Use cached state | 0ms | May be stale |
| Proceed to Checkout | Hard validation (real-time) | +50-100ms | 100% accurate |
| Submit Order | Reserve with timeout | +100-200ms | 100% accurate |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
class CartInventoryService { // Level 1: Fast check using cached inventory async quickAvailabilityCheck(productId: string, variantId: string): Promise<StockStatus> { // Redis cache of inventory - updated every 30 seconds const cachedStock = await this.redis.get( `inventory:quick:${productId}:${variantId}` ); if (cachedStock) { const stock = parseInt(cachedStock); if (stock > 10) return 'in_stock'; if (stock > 0) return 'low_stock'; return 'out_of_stock'; } // Cache miss - fall back to inventory service return this.getActualStock(productId, variantId); } // Level 2: Async validation triggered on cart view async triggerAsyncValidation(cart: Cart): Promise<void> { // Don't block - fire and forget setImmediate(async () => { try { const validationResults = await this.validateCartItems(cart); if (validationResults.hasChanges) { // Update cart state in Redis cart.validationState = validationResults.state; cart.requiresRevalidation = false; await this.cartRepo.saveCart(cart); // Push notification to client via WebSocket if connected await this.notifyCartChange(cart.userId || cart.sessionId, { type: 'CART_VALIDATED', changes: validationResults.changes, }); } } catch (error) { // Don't crash on validation failure console.error('Async validation failed:', error); } }); } // Level 3: Hard validation for checkout async validateForCheckout(cart: Cart): Promise<CheckoutValidationResult> { const results: ItemValidationResult[] = []; // Check all items in parallel const validations = await Promise.all( cart.items.map(async (item) => { const inventory = await this.inventoryService.getExactStock( item.productId, item.variantId, { includeReserved: true } ); return { itemId: item.id, productId: item.productId, variantId: item.variantId, requestedQuantity: item.quantity, availableQuantity: inventory.available, totalStock: inventory.total, reserved: inventory.reserved, }; }) ); const errors: CheckoutBlocker[] = []; const warnings: CheckoutWarning[] = []; const adjustments: QuantityAdjustment[] = []; for (const v of validations) { if (v.availableQuantity === 0) { errors.push({ type: 'OUT_OF_STOCK', itemId: v.itemId, message: 'This item is no longer available', suggestedAction: 'REMOVE_ITEM', }); } else if (v.availableQuantity < v.requestedQuantity) { adjustments.push({ itemId: v.itemId, originalQuantity: v.requestedQuantity, adjustedQuantity: v.availableQuantity, reason: 'INSUFFICIENT_STOCK', }); warnings.push({ type: 'QUANTITY_REDUCED', itemId: v.itemId, message: `Only ${v.availableQuantity} available`, }); } else if (v.availableQuantity - v.requestedQuantity < 5) { warnings.push({ type: 'LOW_STOCK', itemId: v.itemId, message: 'Only a few left in stock', }); } } return { canProceed: errors.length === 0, errors, warnings, adjustments, requiresUserConfirmation: adjustments.length > 0, }; } // Level 4: Inventory reservation at order submission async reserveInventory(cart: Cart, orderId: string): Promise<ReservationResult> { const reservations: InventoryReservation[] = []; // Reserve each item with idempotency key based on orderId for (const item of cart.items) { const reservation = await this.inventoryService.reserve({ productId: item.productId, variantId: item.variantId, quantity: item.quantity, reservationId: `${orderId}-${item.id}`, // Idempotent expiresAt: Date.now() + 15 * 60 * 1000, // 15 min hold }); if (!reservation.success) { // Rollback previous reservations for (const r of reservations) { await this.inventoryService.releaseReservation(r.reservationId); } return { success: false, failedItem: item.id, reason: reservation.reason, }; } reservations.push(reservation); } return { success: true, reservations, expiresAt: Date.now() + 15 * 60 * 1000, }; }}This design is 'optimistic'—we allow adding to cart without hard checks. The alternative 'pessimistic' approach reserves inventory at add-to-cart, but this causes problems: abandoned carts hold inventory for hours, flash sales exhaust stock for non-buyers. The optimistic approach with hard validation at checkout provides better user experience for the vast majority of legitimate purchases.
Prices and promotions add significant complexity to cart logic. Prices change frequently (sales, dynamic pricing), and promotions have complex eligibility rules. The cart must:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
class CartPromotionEngine { // Order of promotion application matters! private promotionPipeline = [ 'PRODUCT_SALE', // Base sale prices 'CATEGORY_DISCOUNT', // e.g., 20% off Electronics 'QUANTITY_DISCOUNT', // e.g., Buy 3 get 10% off 'BOGO', // Buy one get one 'PROMO_CODE', // User-entered codes 'MEMBERSHIP', // Prime-style membership 'CART_LEVEL', // e.g., $20 off orders over $100 'SHIPPING', // Free shipping promotions ]; async applyPromotions(cart: Cart): Promise<Cart> { // Reset applied discounts for (const item of cart.items) { item.appliedDiscounts = []; } cart.appliedPromoCodes = cart.appliedPromoCodes.filter(p => !p.autoApplied); let runningTotal = this.calculateSubtotal(cart); const appliedPromotions: AppliedPromotion[] = []; for (const promotionType of this.promotionPipeline) { const eligiblePromotions = await this.getEligiblePromotions( cart, promotionType, appliedPromotions // Check for conflicts ); for (const promo of eligiblePromotions) { const application = this.applyPromotion(cart, promo, runningTotal); if (application.applied) { appliedPromotions.push({ promotion: promo, discount: application.discount, appliedTo: application.appliedItems, }); runningTotal = runningTotal - application.discount.amount; } } } // Calculate final totals cart.discount = { amount: appliedPromotions.reduce((sum, p) => sum + p.discount.amount, 0), currency: cart.items[0]?.currentPrice.currency || 'USD', }; cart.subtotal = this.calculateSubtotal(cart); cart.total = { amount: cart.subtotal.amount - cart.discount.amount + cart.estimatedShipping.amount + cart.estimatedTax.amount, currency: cart.subtotal.currency, }; return cart; } async applyPromoCode(cart: Cart, code: string): Promise<PromoCodeResult> { // Validate code exists and is active const promo = await this.promoService.getByCode(code); if (!promo) { return { success: false, error: 'INVALID_CODE' }; } if (promo.expiresAt < Date.now()) { return { success: false, error: 'EXPIRED' }; } if (promo.usageCount >= promo.maxUsage) { return { success: false, error: 'MAX_USAGE_REACHED' }; } // Check user-specific limits if (cart.userId) { const userUsage = await this.promoService.getUserUsageCount( promo.id, cart.userId ); if (userUsage >= promo.maxUsagePerUser) { return { success: false, error: 'ALREADY_USED' }; } } // Check cart meets minimum requirements const subtotal = this.calculateSubtotal(cart); if (promo.minimumOrderValue && subtotal.amount < promo.minimumOrderValue) { return { success: false, error: 'MINIMUM_NOT_MET', data: { minimum: promo.minimumOrderValue, current: subtotal.amount } }; } // Check for conflicts with existing promos for (const existing of cart.appliedPromoCodes) { if (!this.canStack(existing, promo)) { return { success: false, error: 'CONFLICTS_WITH_EXISTING', data: { conflictingCode: existing.code } }; } } // Check product eligibility const eligibleItems = this.getEligibleItems(cart, promo); if (eligibleItems.length === 0) { return { success: false, error: 'NO_ELIGIBLE_ITEMS' }; } // Apply the code cart.appliedPromoCodes.push({ code: promo.code, promoId: promo.id, type: promo.type, autoApplied: false, }); // Recalculate with new promo await this.applyPromotions(cart); return { success: true, discount: cart.discount, message: promo.successMessage || `${promo.code} applied!` }; }}Cart abandonment rates average 70-80% across e-commerce. This represents massive unrealized revenue. The cart service plays a crucial role in abandonment recovery through:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
class CartAbandonmentTracker { // Publish cart events to analytics pipeline async trackCartEvent(event: CartEvent): Promise<void> { const abandonmentEvent = { eventId: generateUUID(), eventType: event.type, timestamp: Date.now(), // Cart context cartId: event.cart.id, userId: event.cart.userId, sessionId: event.cart.sessionId, // Cart state itemCount: event.cart.items.length, cartValue: event.cart.total.amount, items: event.cart.items.map(item => ({ productId: item.productId, variantId: item.variantId, quantity: item.quantity, price: item.currentPrice.amount, category: item.productSnapshot.category, })), // Funnel stage stage: this.determineStage(event), // Device/session info device: event.context.device, referrer: event.context.referrer, }; await this.kafka.produce('cart-events', abandonmentEvent); } private determineStage(event: CartEvent): FunnelStage { switch (event.type) { case 'ITEM_ADDED': return 'CART'; case 'CHECKOUT_STARTED': return 'CHECKOUT'; case 'SHIPPING_SET': return 'SHIPPING'; case 'PAYMENT_STARTED': return 'PAYMENT'; case 'ORDER_COMPLETED': return 'CONVERTED'; case 'SESSION_ENDED': return 'ABANDONED'; default: return 'UNKNOWN'; } }} // Downstream: Abandonment recovery serviceclass AbandonmentRecoveryService { async processAbandonedCart(cart: AbandonedCart): Promise<void> { // Only recover for authenticated users with email if (!cart.userId) return; const user = await this.userService.get(cart.userId); if (!user.email || !user.marketingConsent) return; // Check if already recovered or converted const cartStatus = await this.getCartStatus(cart.cartId); if (cartStatus === 'converted' || cartStatus === 'recovered') return; // Determine recovery strategy based on cart value and user tier const strategy = this.selectRecoveryStrategy(cart, user); // Schedule recovery emails if (strategy.emails) { await this.scheduleEmail(user.email, { template: 'cart_abandonment', delay: '1h', data: { cartItems: cart.items, cartTotal: cart.total, restoreUrl: this.generateRestoreUrl(cart.cartId, user.id), } }); // Follow-up with discount if high value if (cart.total.amount > 10000 && user.tier !== 'prime') { await this.scheduleEmail(user.email, { template: 'cart_abandonment_discount', delay: '24h', data: { discount: '10%', promoCode: await this.generatePersonalPromo(user.id, cart.cartId), } }); } } } private generateRestoreUrl(cartId: string, userId: string): string { // Create signed, time-limited URL that restores cart in one click const token = this.signToken({ cartId, userId }, '7d'); return `https://example.com/cart/restore?token=${token}`; }}We've explored the complete architecture of a shopping cart system designed for massive scale. Here are the key principles:
What's Next:
With catalog and cart architecture covered, we'll dive into Inventory Management—the 'hot path' data that connects carts to actual physical goods. You'll learn about real-time stock tracking, distributed inventory across fulfillment centers, and the reservation systems that prevent overselling during high-traffic events.
You now understand how to architect a shopping cart that handles millions of concurrent sessions, persists across devices, validates inventory in real-time, and recovers abandoned purchases. The key insight: cart design is about managing state across an inherently stateless web with speed, accuracy, and user experience as competing priorities.