Caching Strategies

Multi-level caching strategies for optimal performance including browser, application, and Redis caching.

Caching Strategies

Implement multi-level caching strategies to dramatically improve application performance and reduce server load.

Multi-Level Caching Architecture

Browser Caching

Control how browsers cache your application resources:

// app/api/products/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const product = await getProduct(params.id)
  
  return Response.json(product, {
    headers: {
      // Cache for 5 minutes, stale-while-revalidate for 1 hour
      'Cache-Control': 'public, max-age=300, stale-while-revalidate=3600',
      'ETag': generateETag(product),
      'Last-Modified': product.updated_at,
    }
  })
}

Application-Level Caching

Implement in-memory caching for frequently accessed data:

// lib/cache/memory-cache.ts
class MemoryCache {
  private cache = new Map<string, { data: any; expiry: number }>()
  
  set(key: string, data: any, ttl: number = 300000) { // 5 minutes default
    const expiry = Date.now() + ttl
    this.cache.set(key, { data, expiry })
  }
  
  get(key: string) {
    const item = this.cache.get(key)
    if (!item) return null
    
    if (Date.now() > item.expiry) {
      this.cache.delete(key)
      return null
    }
    
    return item.data
  }
  
  clear() {
    this.cache.clear()
  }
}

export const memoryCache = new MemoryCache()

Redis Caching

Distribute cache across multiple server instances:

// lib/cache/redis-cache.ts
import Redis from 'ioredis'

const redis = new Redis(process.env.REDIS_URL)

export class RedisCache {
  static async get(key: string) {
    try {
      const value = await redis.get(key)
      return value ? JSON.parse(value) : null
    } catch (error) {
      console.error('Redis get error:', error)
      return null
    }
  }
  
  static async set(key: string, value: any, ttl: number = 300) {
    try {
      await redis.setex(key, ttl, JSON.stringify(value))
    } catch (error) {
      console.error('Redis set error:', error)
    }
  }
  
  static async invalidate(pattern: string) {
    try {
      const keys = await redis.keys(pattern)
      if (keys.length > 0) {
        await redis.del(...keys)
      }
    } catch (error) {
      console.error('Redis invalidate error:', error)
    }
  }
}

Cache Invalidation Strategy

Smart Cache Invalidation

Implement intelligent cache invalidation to maintain data consistency:

// lib/cache/invalidation.ts
export class CacheInvalidation {
  static async invalidateProduct(productId: string) {
    // Invalidate specific product caches
    await RedisCache.invalidate(`product:${productId}:*`)
    await RedisCache.invalidate(`inventory:product:${productId}:*`)
    
    // Invalidate related list caches
    await RedisCache.invalidate('products:list:*')
    await RedisCache.invalidate('inventory:summary:*')
    
    // Revalidate Next.js cache tags
    revalidateTag('products')
    revalidateTag('inventory')
  }
  
  static async invalidateInventory(warehouseId: string) {
    await RedisCache.invalidate(`inventory:warehouse:${warehouseId}:*`)
    await RedisCache.invalidate('dashboard:metrics:*')
    
    revalidateTag('inventory')
    revalidateTag('dashboard')
  }
}

Cache Patterns

Cache-Aside Pattern

Load data on demand and cache the results:

async function getProductWithCache(productId: string) {
  const cacheKey = `product:${productId}`
  
  // Try cache first
  let product = await RedisCache.get(cacheKey)
  if (product) {
    return product
  }
  
  // Load from database
  product = await getProductFromDatabase(productId)
  
  // Cache the result
  await RedisCache.set(cacheKey, product, 300) // 5 minutes
  
  return product
}

Write-Through Pattern

Update cache immediately when data changes:

async function updateProduct(productId: string, updates: Partial<Product>) {
  // Update database
  const updatedProduct = await updateProductInDatabase(productId, updates)
  
  // Update cache immediately
  const cacheKey = `product:${productId}`
  await RedisCache.set(cacheKey, updatedProduct, 300)
  
  return updatedProduct
}

Cache Warming

Pre-populate cache with frequently accessed data:

async function warmCache() {
  // Warm popular products
  const popularProducts = await getPopularProducts()
  for (const product of popularProducts) {
    await RedisCache.set(`product:${product.id}`, product, 600)
  }
  
  // Warm dashboard metrics
  const metrics = await getDashboardMetrics()
  await RedisCache.set('dashboard:metrics', metrics, 300)
}

Best Practices

Cache Key Design

  • Use hierarchical keys: product:123:details
  • Include version information: product:123:v2
  • Use consistent naming conventions

TTL Strategy

  • Static data: 24 hours
  • Semi-static data: 1-6 hours
  • Dynamic data: 5-30 minutes
  • Real-time data: 30 seconds - 5 minutes

Cache Monitoring

Monitor cache hit rates and performance:

export class CacheMetrics {
  private static hits = 0
  private static misses = 0
  
  static recordHit() {
    this.hits++
  }
  
  static recordMiss() {
    this.misses++
  }
  
  static getHitRate() {
    const total = this.hits + this.misses
    return total > 0 ? this.hits / total : 0
  }
}

Effective caching strategies can improve application performance by 5-10x while reducing database load and server costs.