E-commerce Shopping Cart Caching Strategies
Shopping carts are accessed constantly—add item, view cart, update quantity, check out. Every interaction must be instant. Slow carts kill conversions. Here's how to build a caching strategy that keeps carts fast and reliable.
The Cart Caching Challenge
Carts are tricky to cache because they:
- Change frequently (every add/remove)
- Need real-time inventory validation
- Must persist across sessions and devices
- Require price updates during sales events
The solution is a hybrid approach: cache for speed, database for durability.
Cart Data Structure
Use Redis hashes for efficient cart operations:
// Cart structure in Redis
// Key: cart:{userId}
// Hash fields: product IDs
// Hash values: JSON with quantity, price snapshot, metadata
await redis.hset(`cart:${userId}`, productId, JSON.stringify({
quantity: 2,
priceAtAdd: 29.99,
addedAt: Date.now(),
variant: 'blue-xl'
}));
// Get entire cart in one operation
const cart = await redis.hgetall(`cart:${userId}`);
// Get cart item count
const itemCount = await redis.hlen(`cart:${userId}`);
Why hashes? Update one item without touching others. Get the entire cart in one round-trip. Perfect for cart operations.
Inventory Integration
Validate inventory on every cart view, but cache the result briefly:
async function getCartWithInventory(userId) {
const cart = await redis.hgetall(`cart:${userId}`);
// Check inventory (with short cache)
const items = await Promise.all(
Object.entries(cart).map(async ([productId, data]) => {
const item = JSON.parse(data);
// Get inventory (cached for 30 seconds)
const inventory = await getInventoryCached(productId);
return {
productId,
...item,
inStock: inventory.available >= item.quantity,
currentPrice: inventory.price,
priceChanged: inventory.price !== item.priceAtAdd
};
})
);
return items;
}
async function getInventoryCached(productId) {
const cacheKey = `inventory:${productId}`;
let inventory = await redis.get(cacheKey);
if (!inventory) {
inventory = await db.query(
'SELECT available, price FROM products WHERE id = $1',
[productId]
);
await redis.set(cacheKey, JSON.stringify(inventory), 'EX', 30);
}
return typeof inventory === 'string' ? JSON.parse(inventory) : inventory;
}
Multi-Device Cart Sync
When users log in, merge anonymous cart with saved cart:
async function mergeCartsOnLogin(userId, anonymousCartId) {
const anonCart = await redis.hgetall(`cart:anon:${anonymousCartId}`);
const userCart = await redis.hgetall(`cart:${userId}`);
// Merge: user cart items take priority, add new items from anon
for (const [productId, data] of Object.entries(anonCart)) {
if (!userCart[productId]) {
await redis.hset(`cart:${userId}`, productId, data);
}
}
// Delete anonymous cart
await redis.del(`cart:anon:${anonymousCartId}`);
// Trigger cart merge event for analytics
await events.emit('cart:merged', { userId, itemsAdded: Object.keys(anonCart).length });
}
Price Update Handling
During sales events, prices change but cart should show current prices:
async function recalculateCartPrices(userId) {
const cart = await redis.hgetall(`cart:${userId}`);
const productIds = Object.keys(cart);
// Batch fetch current prices
const prices = await db.query(
'SELECT id, price, sale_price FROM products WHERE id = ANY($1)',
[productIds]
);
const priceMap = new Map(prices.rows.map(p => [p.id, p.sale_price || p.price]));
// Calculate totals
let subtotal = 0;
const items = [];
for (const [productId, data] of Object.entries(cart)) {
const item = JSON.parse(data);
const currentPrice = priceMap.get(productId);
items.push({
productId,
quantity: item.quantity,
originalPrice: item.priceAtAdd,
currentPrice,
savings: (item.priceAtAdd - currentPrice) * item.quantity
});
subtotal += currentPrice * item.quantity;
}
return { items, subtotal, itemCount: items.length };
}
Cart Persistence Strategy
Balance speed with durability:
// Write to cache immediately, database async
async function addToCart(userId, productId, quantity) {
const item = {
quantity,
priceAtAdd: await getCurrentPrice(productId),
addedAt: Date.now()
};
// Immediate: Update cache
await redis.hset(`cart:${userId}`, productId, JSON.stringify(item));
// Background: Persist to database
setImmediate(async () => {
await db.query(`
INSERT INTO cart_items (user_id, product_id, quantity, added_at)
VALUES ($1, $2, $3, NOW())
ON CONFLICT (user_id, product_id)
DO UPDATE SET quantity = $3
`, [userId, productId, quantity]);
});
// Set cart expiration (7 days)
await redis.expire(`cart:${userId}`, 7 * 24 * 60 * 60);
}
Abandoned Cart Recovery
Track cart activity for recovery campaigns:
// On each cart interaction
async function trackCartActivity(userId) {
await redis.zadd('cart:activity', Date.now(), userId);
}
// Find abandoned carts (no activity for 1 hour, not checked out)
async function findAbandonedCarts() {
const oneHourAgo = Date.now() - 60 * 60 * 1000;
const abandonedUserIds = await redis.zrangebyscore(
'cart:activity',
0,
oneHourAgo
);
return abandonedUserIds.filter(async (userId) => {
const cartSize = await redis.hlen(`cart:${userId}`);
return cartSize > 0;
});
}
Faster carts, higher conversions
Cachee.ai powers sub-10ms cart operations for high-traffic e-commerce stores.
Start Free Trial