# Shopping Cart: Build E-commerce Cart Logic

# E-commerce Shopping Cart Logic: Session & Persistence

## Problem

Building a robust e-commerce shopping cart requires handling:
- **State Management**: Track items, quantities, prices across user sessions
- **Persistence**: Maintain cart data across page refreshes and browser closures
- **Session Handling**: Distinguish between anonymous and authenticated users
- **Data Consistency**: Ensure accurate calculations and inventory tracking
- **Performance**: Minimize database queries and optimize retrieval

## Solution Architecture

### Core Components

1. **Cart Data Model**: Structure for items, metadata, and calculations
2. **Session Management**: Server-side session tracking with unique identifiers
3. **Persistence Layer**: Database/cache storage for durability
4. **API Endpoints**: CRUD operations for cart manipulation
5. **Client-Side Integration**: Synchronization between frontend and backend

### Key Strategies

- **Hybrid Approach**: Session storage + database persistence
- **Optimistic Updates**: Immediate UI feedback with server validation
- **Conflict Resolution**: Handle concurrent modifications gracefully
- **Expiration Policies**: Clean up abandoned carts automatically

---

## Code Implementation

### 1. Backend: Node.js/Express with MongoDB

```javascript
// models/Cart.js
const mongoose = require('mongoose');

const cartItemSchema = new mongoose.Schema({
  productId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Product',
    required: true
  },
  quantity: {
    type: Number,
    required: true,
    min: 1,
    default: 1
  },
  price: Number, // Snapshot of price at time of addition
  addedAt: {
    type: Date,
    default: Date.now
  }
});

const cartSchema = new mongoose.Schema({
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    sparse: true // Null for anonymous users
  },
  sessionId: {
    type: String,
    required: true,
    unique: true,
    index: true
  },
  items: [cartItemSchema],
  createdAt: {
    type: Date,
    default: Date.now
  },
  updatedAt: {
    type: Date,
    default: Date.now
  },
  expiresAt: {
    type: Date,
    default: () => new Date(Date.now() + 30 * 24 * 60 * 60 * 1000), // 30 days
    index: { expireAfterSeconds: 0 } // TTL index
  }
});

// Middleware to update timestamp
cartSchema.pre('save', function(next) {
  this.updatedAt = new Date();
  next();
});

module.exports = mongoose.model('Cart', cartSchema);
```

```javascript
// services/CartService.js
const Cart = require('../models/Cart');
const Product = require('../models/Product');

class CartService {
  // Initialize or retrieve cart
  async getOrCreateCart(sessionId, userId = null) {
    let cart = await Cart.findOne({ sessionId });
    
    if (!cart) {
      cart = new Cart({
        sessionId,
        userId,
        items: []
      });
      await cart.save();
    }
    
    return cart;
  }

  // Add item to cart
  async addItem(sessionId, productId, quantity = 1, userId = null) {
    const product = await Product.findById(productId);
    
    if (!product) {
      throw new Error('Product not found');
    }
    
    if (product.stock < quantity) {
      throw new Error('Insufficient stock');
    }

    let cart = await this.getOrCreateCart(sessionId, userId);
    
    // Check if item already exists
    const existingItem = cart.items.find(
      item => item.productId.toString() === productId
    );

    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      cart.items.push({
        productId,
        quantity,
        price: product.price
      });
    }

    await cart.save();
    return cart.populate('items.productId');
  }

  // Update item quantity
  async updateItemQuantity(sessionId, productId, quantity) {
    if (quantity < 0) {
      throw new Error('Quantity cannot be negative');
    }

    const cart = await Cart.findOne({ sessionId });
    
    if (!cart) {
      throw new Error('Cart not found');
    }

    const item = cart.items.find(
      i => i.productId.toString() === productId
    );

    if (!item) {
      throw new Error('Item not in cart');
    }

    if (quantity === 0) {
      cart.items = cart.items.filter(
        i => i.productId.toString() !== productId
      );
    } else {
      item.quantity = quantity;
    }

    await cart.save();
    return cart.populate('items.productId');
  }

  // Remove item from cart
  async removeItem(sessionId, productId) {
    const cart = await Cart.findOne({ sessionId });
    
    if (!cart) {
      throw new Error('Cart not found');
    }

    cart.items = cart.items.filter(
      item => item.productId.toString() !== productId
    );

    await cart.save();
    return cart.populate('items.productId');
  }

  // Clear entire cart
  async clearCart(sessionId) {
    const cart = await Cart.findOne({ sessionId });
    
    if (!cart) {
      throw new Error('Cart not found');
    }

    cart.items = [];
    await cart.save();
    return cart;
  }

  // Calculate totals
  async getCartSummary(sessionId) {
    const cart = await Cart.findOne({ sessionId })
      .populate('items.productId');

    if (!cart) {
      return {
        itemCount: 0,
        subtotal: 0,
        tax: 0,
        total: 0,
        items: []
      };
    }

    const subtotal = cart.items.reduce((sum, item) => {
      return sum + (item.price * item.quantity);
    }, 0);

    const tax = subtotal * 0.1; // 10% tax
    const total = subtotal + tax;

    return {
      itemCount: cart.items.length,
      subtotal: parseFloat(subtotal.toFixed(2)),
      tax: parseFloat(tax.toFixed(2)),
      total: parseFloat(total.toFixed(2)),
      items: cart.items
    };
  }

  // Merge carts (for login scenario)
  async mergeCarts(anonymousSessionId, userId) {
    const anonymousCart = await Cart.findOne({ 
      sessionId: anonymousSessionId 
    });
    
    const userCart = await Cart.findOne({ userId });

    if (!anonymousCart) {
      return userCart;
    }

    if (!userCart) {
      anonymousCart.userId = userId;
      await anonymousCart.save();
      return anonymousCart;
    }

    // Merge items
    for (const anonItem of anonymousCart.items) {
      const existingItem = userCart.items.find(
        item => item.productId.toString() === anonItem.productId.toString()
      );

      if (existingItem) {
        existingItem.quantity += anonItem.quantity;
      } else {
        userCart.items.push(anonItem);
      }
    }

    await userCart.save();
    await Cart.deleteOne({ sessionId: anonymousSessionId });

    return userCart;
  }
}

module.exports = new CartService();
```

```javascript
// middleware/sessionMiddleware.js
const crypto = require('crypto');

const SESSION_COOKIE_NAME = 'sessionId';
const SESSION_DURATION = 30 * 24 * 60 * 60 * 1000; // 30 days

function generateSessionId() {
  return crypto.randomBytes(32).toString('hex');
}

function sessionMiddleware(req, res, next) {
  let sessionId = req.cookies[SESSION_COOKIE_NAME];

  if (!sessionId) {
    sessionId = generateSessionId();
    res.cookie(SESSION_COOKIE_NAME, sessionId, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: SESSION_DURATION
    });
  }

  req.sessionId = sessionId;
  req.userId = req.user?.id || null;
  next();
}

module.exports = sessionMiddleware;
```

```javascript
// routes/cartRoutes.js
const express = require('express');
const CartService = require('../services/CartService');
const { authenticate } = require('../middleware/auth');

const router = express.Router();

// Get cart
router.get('/', async (req, res) => {
  try {
    const summary = await CartService.getCartSummary(req.sessionId);
    res.json(summary);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Add item
router.post('/items', async (req, res) => {
  try {
    const { productId, quantity = 1 } = req.body;
    const cart = await CartService.addItem(
      req.sessionId,
      productId,
      quantity,
      req.userId
    );
    res.json(cart);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Update item quantity
router.patch('/items/:productId', async (req, res) => {
  try {
    const { quantity } = req.body;
    const cart = await CartService.updateItemQuantity(
      req.sessionId,
      req.params.productId,
      quantity
    );
    res.json(cart);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Remove item
router.delete('/items/:productId', async (req, res) => {
  try {
    const cart = await CartService.removeItem(
      req.sessionId,
      req.params.productId
    );
    res.json(cart);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Clear cart
router.delete('/', async (req, res) => {
  try {
    await CartService.clearCart(req.sessionId);
    res.json({ message: 'Cart cleared' });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// Merge carts on login
router.post('/merge', authenticate, async (req, res) => {
  try {
    const { anonymousSessionId } = req.body;
    const cart = await CartService.mergeCarts(
      anonymousSessionId,
      req.userId
    );
    res.json(cart);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

module.exports = router;
```

### 2. Frontend: React with Persistence

```javascript
// hooks/useCart.js
import { useState, useEffect, useCallback, useRef } from 'react';

const CART_API = '/api/cart';
const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes

export function useCart() {
  const [cart, setCart] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const syncTimerRef = useRef(null);

  // Fetch cart from server
  const fetchCart = useCallback(async () => {
    try {
      setLoading(true);
      const response = await fetch(CART_API);
      const data = await response.json();
      setCart(data);
      setError(null);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, []);

  // Initialize cart on mount
  useEffect(() => {
    fetchCart();

    // Set up periodic sync
    syncTimerRef.current = setInterval(fetchCart, SYNC_INTERVAL);

    return () => clearInterval(syncTimerRef.current);
  }, [fetchCart]);

  // Add item to cart
  const addItem = useCallback(async (productId, quantity = 1) => {
    try {
      // Optimistic update
      setCart(prev => ({
        ...prev,
        itemCount: prev.itemCount + 1
      }));

      const response = await fetch(`${CART_API}/items`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ productId, quantity })
      });

      if (!response.ok) throw new Error('Failed to add item');

      const data = await response.json();
      setCart(data);
    } catch (err) {
      setError(err.message);
      fetchCart(); // Revert to server state
    }
  }, [fetchCart]);

  // Update item quantity
  const updateQuantity = useCallback(async (productId, quantity) => {
    try {
      const response = await fetch(`${CART_API}/items/${productId}`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ quantity })
      });

      if (!response.ok) throw new Error('Failed to update quantity');

      const data = await response.json();
      setCart(data);
    } catch (err) {
      setError(err.message);
      fetchCart();
    }
  }, [fetchCart]);

  // Remove item
  const removeItem = useCallback(async (productId) => {
    try {
      const response = await fetch(`${CART_API}/items/${productId}`, {
        method: 'DELETE'
      });

      if (!response.ok) throw new Error('Failed to remove item');

      const data = await response.json();
      setCart(data);
    } catch (err) {
      setError(err.message);
      fetchCart();
    }
  }, [fetchCart]);

  // Clear cart
  const clearCart = useCallback(async () => {
    try {
      const response = await fetch(CART_API, { method: 'DELETE' });

      if (!response.ok) throw new Error('Failed to clear cart');

      setCart({
        itemCount: 0,
        subtotal: 0,
        tax: 0,
        total: 0,
        items: []
      });
    } catch (err) {
      setError(err.message);
      fetchCart();
    }
  }, [fetchCart]);

  return {
    cart,
    loading,
    error,
    addItem,
    updateQuantity,
    removeItem,
    clearCart,
    refetch: fetchCart
  };
}
```

```javascript
// components/ShoppingCart.jsx
import React from 'react';
import { useCart } from '../hooks/useCart';

export function ShoppingCart() {
  const { cart, loading, error, updateQuantity, removeItem, clearCart } = useCart();

  if (loading) return <div>Loading cart...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!cart || cart.itemCount === 0) return <div>Your cart is empty</div>;

  return (
    <div className="cart-container">
      <h2>Shopping Cart ({cart.itemCount} items)</h2>

      <table className="cart-table">
        <thead>
          <tr>
            <th>Product</th>
            <th>Price</th>
            <th>Quantity</th>
            <th>Total</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          {cart.items.map(item => (
            <tr key={item.productId._id}>
              <td>{item.productId.name}</td>
              <td>${item.price.toFixed(2)}</td>
              <td>
                <input
                  type="number"
                  min="1"
                  value={item.quantity}
                  onChange={(e) =>
                    updateQuantity(item.productId._id, parseInt(e.target.value))
                  }
                />
              </td>
              <td>${(item.price * item.quantity).toFixed(2)}</td>
              <td>
                <button onClick={() => removeItem(item.productId._id)}>
                  Remove
                </button>
              </td>
            </tr>
          ))}
        </tbody>
      </table>

      <div className="cart-summary">
        <p>Subtotal: ${cart.subtotal.toFixed(2)}</p>
        <p>Tax: ${cart.tax.toFixed(2)}</p>
        <h3>Total: ${cart.total.toFixed(2)}</h3>
      </div>

      <button onClick={clearCart} className="btn-clear">
        Clear Cart
      </button>
      <button className="btn-checkout">Proceed to Checkout</button>
    </div>
  );
}
```

### 3. Local Storage Fallback (Client-Side Caching)

```javascript
// utils/cartCache.js
const CACHE_KEY = 'cart_cache';
const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours

export const cartCache = {
  set: (data) => {
    const cacheData = {
      data,
      timestamp: Date.now()
    };
    localStorage.setItem(CACHE_KEY, JSON.stringify(cacheData));
  },

  get: () => {
    const cached = localStorage.getItem(CACHE_KEY);
    if (!cached) return null;

    const { data, timestamp } = JSON.parse(cached);
    
    if (Date.now() - timestamp > CACHE_EXPIRY) {
      localStorage.removeItem(CACHE_KEY);
      return null;
    }

    return data;
  },

  clear: () => {
    localStorage.removeItem(CACHE_KEY);
  }
};

// Usage in hook
export function useCart() {
  const [cart, setCart] = useState(() => cartCache.get());

  const fetchCart = useCallback(async () => {
    try {
      const response = await fetch(CART_API);
      const data = await response.json();
      setCart(data);
      cartCache.set(data);
    } catch (err) {
      // Use cached data if fetch fails
      const cached = cartCache.get();
      if (cached) setCart(cached);
    }
  }, []);

  // ... rest of hook
}
```

---

## Tips & Best Practices

### 1. **Session Management**
- Use **httpOnly cookies** to prevent XSS attacks
- Implement **session expiration** with TTL indexes
- Generate **cryptographically secure** session IDs
- Store session data server-side, not in JWT

### 2. **Data Consistency**
- Validate **stock availability** before adding items
- Use **database transactions** for multi-step operations
- Implement **optimistic locking** for concurrent updates
- Snapshot prices at time of addition (prevents price manipulation)

### 3. **Performance Optimization**
- Use **Redis** for session caching (faster than MongoDB)
- Implement **pagination** for large carts
- Cache product details to reduce lookups
- Batch database operations when possible

### 4. **User Experience**
- Show **loading states** during operations
- Implement **undo/redo** functionality
- Display **real-time stock warnings**
- Persist cart across **browser tabs** using
