← Back to Blog

E-commerce Shopping Cart Caching Strategies

December 22, 2025 • 7 min read • E-commerce

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:

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