Loading learning content...
Inventory management is the most critical—and most challenging—data system in e-commerce. While the catalog is large but read-mostly, and carts are user-scoped, inventory is a globally shared, constantly mutating resource that every transaction must touch.
Consider the scale:
Inventory data is relatively small (~350GB as we calculated), but it represents the hottest data in the entire platform. Every product page view, every add-to-cart, every checkout touches this data. Getting inventory architecture wrong means either:
This page explores how to build an inventory system that threads this needle at massive scale.
By the end of this page, you will understand how to model distributed inventory across fulfillment centers; implement reservation systems that prevent overselling; design for flash sale scenarios with thousands of concurrent purchases; and handle inventory synchronization with warehouse systems.
Inventory isn't a single number per product—it's a distributed quantity across multiple locations, each with different availability characteristics. The data model must capture this complexity while enabling fast queries.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
// Inventory record per SKU per locationinterface InventoryRecord { sku: string; // Product variant identifier locationId: string; // Fulfillment center ID // Quantity breakdown totalOnHand: number; // Physical inventory count allocated: number; // Reserved for confirmed orders reserved: number; // Held for in-progress checkouts available: number; // Actually available = onHand - allocated - reserved // Special quantities damaged: number; // Known damaged, can't sell inTransitIn: number; // Incoming from suppliers inTransitOut: number; // Outgoing transfers to other locations // Thresholds reorderPoint: number; // Trigger replenishment when available < this safetyStock: number; // Minimum to maintain maxStock: number; // Storage capacity limit // Metadata lastCountedAt: Timestamp; // Last physical inventory count lastUpdatedAt: Timestamp; lastOrderedAt: Timestamp; // Last customer purchase // Status sellable: boolean; // Can be sold from this location receivingBlocked: boolean; // Can't receive more inventory} // Aggregated view for customer-facing availabilityinterface ProductAvailability { sku: string; totalAvailable: number; // Sum across all locations availableByRegion: Map<string, number>; overallStatus: 'in_stock' | 'low_stock' | 'out_of_stock' | 'preorder'; // Shipping estimates (depends on customer location) estimatedDelivery: Map<string, DateRange>; // For display stockMessage: string; // "In Stock", "Only 3 left", etc.} // Reservation for checkout holdsinterface InventoryReservation { reservationId: string; // Unique ID (typically orderId-itemId) sku: string; locationId: string; quantity: number; // State status: 'active' | 'confirmed' | 'released' | 'expired'; // Timing createdAt: Timestamp; expiresAt: Timestamp; // Release if not confirmed confirmedAt?: Timestamp; // When order was placed // Context orderId?: string; // After order creation cartId: string; userId?: string;} // Fulfillment center configurationinterface FulfillmentCenter { id: string; name: string; // "Seattle FC1" type: 'warehouse' | 'sortation' | 'delivery_station' | 'store'; // Location address: Address; coordinates: GeoCoordinates; timezone: string; // Capabilities shippingMethods: ShippingMethod[]; handlesHazmat: boolean; handlesOversized: boolean; handlesRefrigerated: boolean; // Regions served serviceRegions: string[]; // Postal code prefixes // Operating hours cutoffTimes: Map<ShippingMethod, Time>; operatingDays: DayOfWeek[]; // Capacity processingCapacity: number; // Orders per day currentUtilization: number;}This seemingly simple formula is the core of inventory management. 'OnHand' is physical count. 'Allocated' is committed to orders. 'Reserved' is held for checkouts in progress. Only 'Available' can be sold. Every checkout must atomically decrement available and increment reserved, then later move from reserved to allocated when order is confirmed.
Inventory storage must balance three competing requirements:
No single database excels at all three. The solution is a multi-layer architecture with different stores optimized for different access patterns.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
┌──────────────────────────────────────────────────────────────────────────────┐│ INVENTORY STORAGE ARCHITECTURE │├──────────────────────────────────────────────────────────────────────────────┤│ ││ ┌─────────────────────────────────────────────────────────────────────────┐ ││ │ LAYER 1: CACHE (DISPLAY) │ ││ │ Redis Cluster │ ││ │ │ ││ │ Purpose: Serve availability for product pages, search results │ ││ │ Consistency: Eventual (30-second lag acceptable) │ ││ │ Latency: <5ms │ ││ │ Pattern: Cache-aside with short TTL │ ││ │ │ ││ │ Key: inventory:display:{sku} │ ││ │ Value: { available: 47, status: "in_stock", message: "In Stock" } │ ││ │ TTL: 30 seconds │ ││ └──────────────────────────────────────────────────────────────────────────┘ ││ │ ││ │ On cache miss or checkout ││ ▼ ││ ┌─────────────────────────────────────────────────────────────────────────┐ ││ │ LAYER 2: HOT STORE (OPERATIONS) │ ││ │ Redis Cluster (Separate from cache) │ ││ │ │ ││ │ Purpose: Handle reservations, atomic decrements, real-time updates │ ││ │ Consistency: Strong within cluster (single-threaded Redis) │ ││ │ Latency: <10ms │ ││ │ Pattern: Lua scripts for atomic operations │ ││ │ │ ││ │ Key: inventory:live:{sku}:{locationId} │ ││ │ Value: { onHand: 50, allocated: 2, reserved: 1, available: 47 } │ ││ │ Persistence: AOF with fsync every second │ ││ └──────────────────────────────────────────────────────────────────────────┘ ││ │ ││ │ Async replication (100ms lag) ││ ▼ ││ ┌─────────────────────────────────────────────────────────────────────────┐ ││ │ LAYER 3: PERSISTENT STORE (SOURCE OF TRUTH) │ ││ │ PostgreSQL / DynamoDB │ ││ │ │ ││ │ Purpose: Durable storage, complex queries, audit trail │ ││ │ Consistency: Strong (SERIALIZABLE for critical ops) │ ││ │ Latency: 20-50ms │ ││ │ Pattern: Event sourcing for audit trail │ ││ │ │ ││ │ Tables: inventory_levels, inventory_transactions, reservations │ ││ │ Features: Row-level locking, transaction history │ ││ └──────────────────────────────────────────────────────────────────────────┘ ││ │└──────────────────────────────────────────────────────────────────────────────┘| Operation | Primary Layer | Fallback | Consistency |
|---|---|---|---|
| Product page availability | Cache | Hot Store | Eventual (30s) |
| Search result stock status | Cache | None (show in_stock) | Eventual |
| Add to cart check | Hot Store | Persistent | Near-real-time |
| Checkout reservation | Hot Store + Persistent | Fail | Strong |
| Order confirmation | Hot Store + Persistent | Fail | Strong |
| Warehouse stock update | Hot Store + Persistent | Queue + Retry | Eventual |
| Inventory reporting | Persistent only | N/A | Consistent snapshot |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
-- Redis Lua script for atomic reservation-- KEYS[1] = inventory key (inventory:live:{sku}:{locationId})-- ARGV[1] = quantity to reserve-- ARGV[2] = reservation ID-- ARGV[3] = expiration time (Unix timestamp)-- Returns: { success: boolean, available: number, reserved: number } local inventoryKey = KEYS[1]local quantity = tonumber(ARGV[1])local reservationId = ARGV[2]local expiresAt = tonumber(ARGV[3]) -- Get current inventorylocal inventory = redis.call('HGETALL', inventoryKey)if #inventory == 0 then return cjson.encode({success = false, error = 'SKU_NOT_FOUND'})end -- Parse into tablelocal inv = {}for i = 1, #inventory, 2 do inv[inventory[i]] = tonumber(inventory[i + 1])end -- Check availabilitylocal available = inv.onHand - inv.allocated - inv.reservedif available < quantity then return cjson.encode({ success = false, error = 'INSUFFICIENT_STOCK', available = available, requested = quantity })end -- Create reservationlocal reservationKey = 'reservation:' .. reservationIdlocal existing = redis.call('EXISTS', reservationKey)if existing == 1 then -- Idempotent - reservation already exists return cjson.encode({success = true, idempotent = true})end -- Update inventory atomicallyredis.call('HINCRBY', inventoryKey, 'reserved', quantity) -- Store reservation detailsredis.call('HMSET', reservationKey, 'sku', KEYS[1]:match(':([^:]+):[^:]+$'), 'locationId', KEYS[1]:match(':([^:]+)$'), 'quantity', quantity, 'status', 'active', 'createdAt', redis.call('TIME')[1], 'expiresAt', expiresAt)redis.call('EXPIREAT', reservationKey, expiresAt) -- Add to expiration sorted set for cleanupredis.call('ZADD', 'reservations:pending', expiresAt, reservationId) return cjson.encode({ success = true, reservationId = reservationId, available = available - quantity, reserved = inv.reserved + quantity})The reservation system is the critical mechanism that prevents overselling during the checkout process. Without reservations, two customers could simultaneously see '1 in stock', both proceed to checkout, and both place orders—but only one can actually receive the item.
Reservation Lifecycle:
12345678910111213141516171819202122232425262728293031
┌─────────────────────────────────────────────────────────────┐ │ RESERVATION LIFECYCLE │ └─────────────────────────────────────────────────────────────┘ Customer clicks Reservation Payment Order "Checkout" created succeeds confirmed │ │ │ │ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ NO │───────▶│ ACTIVE │────▶│ PENDING │────▶│CONFIRMED│ │ RESERVE │ │ │ │ CONFIRM │ │ │ └─────────┘ └────┬────┘ └─────────┘ └─────────┘ ▲ │ │ │ │ │ │ │ │ Timeout or │ │ │ user abandons │ │ │ │ │ │ │ ▼ │ ▼ │ ┌─────────┐ │ ┌─────────┐ └────────────│ RELEASED│◀──────────┘ │ALLOCATED│ │ │ │ (Final) │ └─────────┘ └─────────┘ States: ──────────────────────────────────────────────────────────────── NO RESERVE → Customer browsing, inventory not held ACTIVE → Checkout started, inventory held for 15 min PENDING → Payment processing, hold extended RELEASED → Timeout/abandon, inventory returned CONFIRMED → Order placed, reservation complete ALLOCATED → Order in fulfillment queue123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
class InventoryReservationService { private readonly RESERVATION_TTL = 15 * 60 * 1000; // 15 minutes private readonly PAYMENT_EXTENSION = 5 * 60 * 1000; // +5 min during payment // Reserve inventory for checkout async reserve(request: ReservationRequest): Promise<ReservationResult> { const { orderId, items, locationPreferences } = request; const reservations: InventoryReservation[] = []; try { for (const item of items) { // Find best fulfillment location const location = await this.selectFulfillmentLocation( item.sku, item.quantity, locationPreferences ); if (!location) { // No location has sufficient inventory throw new InsufficientInventoryError(item.sku, item.quantity); } // Atomic reservation via Lua script const reservationId = `${orderId}-${item.sku}-${location.id}`; const result = await this.redis.eval( RESERVE_SCRIPT, [`inventory:live:${item.sku}:${location.id}`], [item.quantity, reservationId, Date.now() + this.RESERVATION_TTL] ); if (!result.success) { throw new InsufficientInventoryError( item.sku, item.quantity, result.available ); } reservations.push({ reservationId, sku: item.sku, locationId: location.id, quantity: item.quantity, status: 'active', expiresAt: new Date(Date.now() + this.RESERVATION_TTL), }); } // Persist to database asynchronously this.persistReservations(reservations).catch(err => { console.error('Reservation persistence failed:', err); // Will be recovered by reconciliation job }); return { success: true, reservations, expiresAt: new Date(Date.now() + this.RESERVATION_TTL), }; } catch (error) { // Rollback any successful reservations for (const reservation of reservations) { await this.release(reservation.reservationId); } throw error; } } // Release reservation (timeout, abandonment, or explicit cancel) async release(reservationId: string): Promise<void> { const releaseScript = ` local reservationKey = 'reservation:' .. ARGV[1] local reservation = redis.call('HGETALL', reservationKey) if #reservation == 0 then return cjson.encode({released = false, reason = 'NOT_FOUND'}) end local res = {} for i = 1, #reservation, 2 do res[reservation[i]] = reservation[i + 1] end if res.status ~= 'active' then return cjson.encode({released = false, reason = 'WRONG_STATUS'}) end local inventoryKey = 'inventory:live:' .. res.sku .. ':' .. res.locationId redis.call('HINCRBY', inventoryKey, 'reserved', -tonumber(res.quantity)) redis.call('HSET', reservationKey, 'status', 'released') redis.call('ZREM', 'reservations:pending', ARGV[1]) return cjson.encode({released = true}) `; await this.redis.eval(releaseScript, [], [reservationId]); await this.db.reservation.update({ where: { id: reservationId }, data: { status: 'released', releasedAt: new Date() }, }); } // Confirm reservation when order is placed async confirm(reservationId: string, orderId: string): Promise<void> { const confirmScript = ` local reservationKey = 'reservation:' .. ARGV[1] local reservation = redis.call('HGETALL', reservationKey) if #reservation == 0 then return cjson.encode({confirmed = false, reason = 'NOT_FOUND'}) end local res = {} for i = 1, #reservation, 2 do res[reservation[i]] = reservation[i + 1] end local inventoryKey = 'inventory:live:' .. res.sku .. ':' .. res.locationId -- Move from reserved to allocated redis.call('HINCRBY', inventoryKey, 'reserved', -tonumber(res.quantity)) redis.call('HINCRBY', inventoryKey, 'allocated', tonumber(res.quantity)) -- Update reservation status redis.call('HMSET', reservationKey, 'status', 'confirmed', 'orderId', ARGV[2], 'confirmedAt', redis.call('TIME')[1] ) redis.call('PERSIST', reservationKey) -- Remove TTL, keep forever redis.call('ZREM', 'reservations:pending', ARGV[1]) return cjson.encode({confirmed = true}) `; await this.redis.eval(confirmScript, [], [reservationId, orderId]); } // Background job: release expired reservations async releaseExpiredReservations(): Promise<number> { const now = Date.now(); // Get expired reservation IDs from sorted set const expiredIds = await this.redis.zrangebyscore( 'reservations:pending', 0, now, 'LIMIT', 0, 1000 ); let released = 0; for (const reservationId of expiredIds) { try { await this.release(reservationId); released++; } catch (error) { console.error(`Failed to release ${reservationId}:`, error); } } return released; }}15 minutes is the typical reservation window—long enough for customers to complete payment, short enough not to tie up inventory. But during flash sales, even 15 minutes can cause problems. Some sites reduce to 5 minutes during high-demand events, with clear countdown timers to customers.
Flash sales and viral products create extreme contention. When 10,000 people try to buy the last 100 PS5s simultaneously, standard inventory systems collapse. Special strategies are needed.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
class FlashSaleInventoryService { // Pattern 1: Pre-split inventory into virtual pools async initializeFlashSale(saleConfig: FlashSaleConfig): Promise<void> { const { sku, totalQuantity, poolCount } = saleConfig; // Split inventory across multiple Redis keys to reduce contention const quantityPerPool = Math.floor(totalQuantity / poolCount); const remainder = totalQuantity % poolCount; for (let i = 0; i < poolCount; i++) { const poolQuantity = i === 0 ? quantityPerPool + remainder : quantityPerPool; await this.redis.set( `flash:inventory:${sku}:pool:${i}`, poolQuantity ); } // Customers are assigned to pools based on hash of their userId // This distributes load while ensuring same user always hits same pool } async reserveFromFlashSale( sku: string, userId: string, poolCount: number ): Promise<FlashReservationResult> { // Deterministic pool assignment const poolIndex = this.hashToPool(userId, poolCount); const poolKey = `flash:inventory:${sku}:pool:${poolIndex}`; // Try primary pool first let result = await this.tryReserveFromPool(poolKey, userId); if (!result.success) { // Primary pool exhausted - try neighboring pools for (let offset = 1; offset < poolCount && !result.success; offset++) { const alternatePool = (poolIndex + offset) % poolCount; const alternateKey = `flash:inventory:${sku}:pool:${alternatePool}`; result = await this.tryReserveFromPool(alternateKey, userId); } } return result; } // Pattern 2: Virtual queue / lottery system async enterFlashSaleLottery( sku: string, userId: string ): Promise<LotteryEntry> { const lotteryKey = `flash:lottery:${sku}`; const entryTime = Date.now(); // Add user to lottery with randomized score (not pure timestamp) // This prevents advantage from precise timing const randomizedScore = entryTime + Math.random() * 1000; await this.redis.zadd(lotteryKey, randomizedScore, userId); // Get user's position const position = await this.redis.zrank(lotteryKey, userId); const totalEntries = await this.redis.zcard(lotteryKey); return { entered: true, position: position + 1, totalEntries, estimatedWait: this.calculateEstimatedWait(position), }; } async processLotteryWinners(sku: string, quantity: number): Promise<void> { const lotteryKey = `flash:lottery:${sku}`; // Process winners in batches const batchSize = 100; let processed = 0; while (processed < quantity) { const winners = await this.redis.zpopmin( lotteryKey, Math.min(batchSize, quantity - processed) ); for (const winnerId of winners) { // Send notification with purchase link await this.notifyWinner(winnerId, sku, { purchaseWindowMinutes: 10, purchaseUrl: this.generateSecurePurchaseUrl(winnerId, sku), }); processed++; } } // Notify losers const remaining = await this.redis.zrange(lotteryKey, 0, -1); for (const loserId of remaining) { await this.notifyLoteryEnd(loserId, sku); } } // Pattern 3: Token bucket rate limiting per SKU async attemptFlashPurchase( sku: string, userId: string ): Promise<PurchaseAttemptResult> { // Rate limit: X purchases per second for this SKU const rateLimitKey = `flash:ratelimit:${sku}`; const userAttemptKey = `flash:attempts:${sku}:${userId}`; // Check if user already purchased const previousPurchase = await this.redis.get(userAttemptKey); if (previousPurchase) { return { success: false, reason: 'ALREADY_PURCHASED' }; } // Token bucket: refill 100 tokens/second, bucket size 100 const allowed = await this.tokenBucket.consume(rateLimitKey, 1); if (!allowed) { return { success: false, reason: 'RATE_LIMITED', retryAfter: 100, // milliseconds }; } // Attempt actual reservation const reservation = await this.reserveFromFlashSale(sku, userId, 10); if (reservation.success) { // Mark user as having purchased await this.redis.setex(userAttemptKey, 86400, 'purchased'); } return reservation; }}With inventory distributed across hundreds of fulfillment centers, choosing where to ship from is as important as whether you have stock. The selection algorithm must optimize for:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
interface FulfillmentSelectionCriteria { // Customer context shippingAddress: Address; requestedDeliveryDate?: Date; shippingSpeed: 'standard' | 'expedited' | 'next_day' | 'same_day'; // Order context items: OrderItem[]; isGift: boolean; // Business rules preferSingleShipment: boolean; // Consolidate items vs parallel ship costWeight: number; // 0-1, how much to prioritize cost vs speed} class FulfillmentSelectionService { async selectFulfillmentLocations( criteria: FulfillmentSelectionCriteria ): Promise<FulfillmentPlan> { const { items, shippingAddress, shippingSpeed, preferSingleShipment } = criteria; // Step 1: Find all locations with sufficient inventory for each item const locationsByItem = await Promise.all( items.map(async item => ({ item, locations: await this.getLocationsWithStock(item.sku, item.quantity), })) ); // Step 2: If single shipment preferred, find locations that have ALL items if (preferSingleShipment) { const consolidatedLocations = this.findConsolidatedLocations(locationsByItem); if (consolidatedLocations.length > 0) { // Score and select best consolidated location return this.selectBestLocation( consolidatedLocations, items, criteria ); } // Fall through to split shipment if consolidation impossible } // Step 3: Optimize across multiple locations const plan = await this.optimizeFulfillmentPlan(locationsByItem, criteria); return plan; } private async optimizeFulfillmentPlan( locationsByItem: ItemLocationMap[], criteria: FulfillmentSelectionCriteria ): Promise<FulfillmentPlan> { const assignments: FulfillmentAssignment[] = []; for (const { item, locations } of locationsByItem) { // Score each location for this item const scoredLocations = await Promise.all( locations.map(async loc => ({ location: loc, score: await this.scoreFulfillmentOption(loc, item, criteria), })) ); // Sort by score (higher is better) scoredLocations.sort((a, b) => b.score.total - a.score.total); // Select best available const selected = scoredLocations[0]; assignments.push({ item, locationId: selected.location.id, estimatedDelivery: selected.score.estimatedDelivery, shippingCost: selected.score.shippingCost, }); } // Group by location for shipment consolidation const shipments = this.groupIntoShipments(assignments); return { shipments, totalShippingCost: shipments.reduce((sum, s) => sum + s.cost, 0), estimatedDelivery: this.latestDeliveryDate(shipments), }; } private async scoreFulfillmentOption( location: FulfillmentCenter, item: OrderItem, criteria: FulfillmentSelectionCriteria ): Promise<FulfillmentScore> { const { shippingAddress, shippingSpeed, costWeight } = criteria; // Calculate delivery estimate const deliveryEstimate = await this.shippingService.estimateDelivery({ origin: location.coordinates, destination: shippingAddress, method: shippingSpeed, weight: item.weight, dimensions: item.dimensions, }); // Calculate shipping cost const shippingCost = await this.shippingService.calculateCost({ origin: location.id, destination: shippingAddress, method: shippingSpeed, items: [item], }); // Score components (0-100 scale) const speedScore = this.scoreDeliverySpeed( deliveryEstimate, criteria.requestedDeliveryDate ); const costScore = 100 - (shippingCost.amount / 50) * 100; // Normalize const utilizationScore = this.scoreUtilization(location); // Prefer underutilized const inventoryScore = this.scoreInventoryDepth(location, item.sku); // Weighted combination const total = speedScore * (1 - costWeight) * 0.4 + costScore * costWeight * 0.3 + utilizationScore * 0.2 + inventoryScore * 0.1; return { total, speedScore, costScore, utilizationScore, inventoryScore, estimatedDelivery: deliveryEstimate, shippingCost, }; }}The e-commerce inventory system doesn't operate in isolation—it must integrate with Warehouse Management Systems (WMS) that track physical inventory movements. This integration is bidirectional:
Inbound from WMS:
Outbound to WMS:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
// Event-driven WMS integration interface WMSEvent { eventId: string; eventType: WMSEventType; timestamp: Timestamp; locationId: string; source: 'wms' | 'inventory_service';} type WMSEventType = | 'RECEIVING_COMPLETE' // New inventory received | 'PICK_COMPLETE' // Items picked for order | 'PACK_COMPLETE' // Items packed | 'SHIP_COMPLETE' // Shipped, inventory no longer in FC | 'CYCLE_COUNT_ADJUSTMENT' // Inventory discrepancy found | 'DAMAGE_REPORT' // Items marked damaged | 'TRANSFER_SHIPPED' // Items sent to another FC | 'TRANSFER_RECEIVED'; // Items received from another FC class WMSEventProcessor { async processEvent(event: WMSEvent): Promise<void> { switch (event.eventType) { case 'RECEIVING_COMPLETE': await this.handleReceiving(event as ReceivingEvent); break; case 'PICK_COMPLETE': await this.handlePickComplete(event as PickEvent); break; case 'SHIP_COMPLETE': await this.handleShipComplete(event as ShipEvent); break; case 'CYCLE_COUNT_ADJUSTMENT': await this.handleCycleCountAdjustment(event as AdjustmentEvent); break; // ... other handlers } } private async handleReceiving(event: ReceivingEvent): Promise<void> { const { locationId, items } = event; for (const item of items) { // Update inventory atomically await this.redis.eval(RECEIVING_SCRIPT, [`inventory:live:${item.sku}:${locationId}`], [item.quantity] ); // Persist to database await this.db.inventoryTransaction.create({ type: 'RECEIVING', sku: item.sku, locationId, quantity: item.quantity, reference: event.purchaseOrderId, }); // Update cache await this.invalidateDisplayCache(item.sku); // Check if this resolves any backorders await this.checkBackorders(item.sku); } } private async handlePickComplete(event: PickEvent): Promise<void> { const { orderId, items } = event; for (const item of items) { // Move from allocated to picked (no longer in sellable inventory) await this.redis.eval(` local key = KEYS[1] redis.call('HINCRBY', key, 'allocated', -tonumber(ARGV[1])) redis.call('HINCRBY', key, 'onHand', -tonumber(ARGV[1])) return redis.call('HGETALL', key) `, [`inventory:live:${item.sku}:${item.locationId}`], [item.quantity] ); } } private async handleCycleCountAdjustment(event: AdjustmentEvent): Promise<void> { const { sku, locationId, actualCount, expectedCount, reason } = event; const variance = actualCount - expectedCount; // Log discrepancy for audit await this.db.inventoryAdjustment.create({ sku, locationId, previousCount: expectedCount, newCount: actualCount, variance, reason, approvedBy: event.approvedBy, auditId: event.auditId, }); // Update inventory await this.redis.hset( `inventory:live:${sku}:${locationId}`, 'onHand', actualCount ); // Alert if variance exceeds threshold if (Math.abs(variance) > 10 || Math.abs(variance / expectedCount) > 0.05) { await this.alertService.send({ type: 'INVENTORY_VARIANCE_ALERT', severity: 'high', data: { sku, locationId, variance, percentVariance: variance / expectedCount }, }); } }}WMS integration is inherently eventually consistent. Physical operations (picking, packing) happen before digital events are transmitted. Design for this lag: don't promise exact counts, use ranges ('Only a few left'), and always validate at checkout even if display showed 'In Stock'.
We've covered the complete architecture of inventory management at e-commerce scale. The key insights:
What's Next:
With inventory management in place, we'll explore Order Processing—the service that orchestrates payment, inventory confirmation, fraud checks, and fulfillment into a reliable, auditable order pipeline. You'll learn about saga patterns, idempotency, and building workflows that can recover from any failure.
You now understand how to architect inventory management for e-commerce at scale. The critical insight: inventory is small but hot, requiring specialized handling. The reservation pattern, atomic operations, and multi-layer caching enable processing thousands of concurrent orders without overselling.