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.