Caching Strategy Overview
Implement effective caching strategies to improve application performance, reduce latency, and decrease load on backend systems.
When to Use Reducing database query load Improving API response times Handling high traffic loads Caching expensive computations Storing session data CDN integration for static assets Implementing distributed caching Rate limiting and throttling Caching Layers ┌─────────────────────────────────────────┐ │ Client Browser Cache │ ├─────────────────────────────────────────┤ │ CDN Cache │ ├─────────────────────────────────────────┤ │ Application Memory Cache │ ├─────────────────────────────────────────┤ │ Distributed Cache (Redis) │ ├─────────────────────────────────────────┤ │ Database │ └─────────────────────────────────────────┘
Implementation Examples 1. Redis Cache Implementation (Node.js) import Redis from 'ioredis';
interface CacheOptions { ttl?: number; // Time to live in seconds prefix?: string; }
class CacheService { private redis: Redis; private defaultTTL = 3600; // 1 hour
constructor(redisUrl: string) { this.redis = new Redis(redisUrl, { retryStrategy: (times) => { const delay = Math.min(times * 50, 2000); return delay; }, maxRetriesPerRequest: 3 });
this.redis.on('connect', () => {
console.log('Redis connected');
});
this.redis.on('error', (error) => {
console.error('Redis error:', error);
});
}
/*
* Get cached value
/
async get
return JSON.parse(value) as T;
} catch (error) {
console.error(`Cache get error for key ${key}:`, error);
return null;
}
}
/*
* Set cached value
/
async set(
key: string,
value: any,
options: CacheOptions = {}
): Promise
if (ttl > 0) {
await this.redis.setex(key, ttl, serialized);
} else {
await this.redis.set(key, serialized);
}
return true;
} catch (error) {
console.error(`Cache set error for key ${key}:`, error);
return false;
}
}
/*
* Delete cached value
/
async delete(key: string): PromiseCache delete error for key ${key}:, error);
return false;
}
}
/*
* Delete multiple keys by pattern
/
async deletePattern(pattern: string): Promise
await this.redis.del(...keys);
return keys.length;
} catch (error) {
console.error(`Cache delete pattern error for ${pattern}:`, error);
return 0;
}
}
/*
* Get or set pattern - fetch from cache or compute and cache
/
async getOrSet
// Fetch and cache
const value = await fetchFn();
await this.set(key, value, options);
return value;
}
/*
* Implement cache-aside pattern with stale-while-revalidate
/
async getStaleWhileRevalidatecache:${key};
const timestampKey = cache:${key}:timestamp;
const [cached, timestamp] = await Promise.all([
this.get<T>(cacheKey),
this.redis.get(timestampKey)
]);
const now = Date.now();
const age = timestamp ? now - parseInt(timestamp) : Infinity;
// Return cached if fresh
if (cached !== null && age < options.ttl * 1000) {
return cached;
}
// Return stale while revalidating in background
if (cached !== null && age < options.staleTime * 1000) {
// Background revalidation
fetchFn()
.then(async (fresh) => {
await this.set(cacheKey, fresh, { ttl: options.ttl });
await this.redis.set(timestampKey, now.toString());
})
.catch(console.error);
return cached;
}
// Fetch fresh data
const fresh = await fetchFn();
await Promise.all([
this.set(cacheKey, fresh, { ttl: options.ttl }),
this.redis.set(timestampKey, now.toString())
]);
return fresh;
}
/*
* Increment counter with TTL
/
async increment(key: string, ttl?: number): Promise
if (count === 1 && ttl) {
await this.redis.expire(key, ttl);
}
return count;
}
/*
* Check if key exists
/
async exists(key: string): Promise
/*
* Get remaining TTL
/
async ttl(key: string): Promise
/*
* Close connection
/
async disconnect(): Promise
// Usage const cache = new CacheService('redis://localhost:6379');
// Simple get/set await cache.set('user:123', { name: 'John', age: 30 }, { ttl: 3600 }); const user = await cache.get('user:123');
// Get or set pattern const posts = await cache.getOrSet( 'posts:recent', async () => { return await database.query('SELECT * FROM posts ORDER BY created_at DESC LIMIT 10'); }, { ttl: 300 } );
// Stale-while-revalidate const data = await cache.getStaleWhileRevalidate( 'expensive-query', async () => await runExpensiveQuery(), { ttl: 300, staleTime: 600 } );
- Cache Decorator (Python) import functools import json import hashlib from typing import Any, Callable, Optional from redis import Redis import time
class CacheDecorator: def init(self, redis_client: Redis, ttl: int = 3600): self.redis = redis_client self.ttl = ttl
def cache_key(self, func: Callable, *args, **kwargs) -> str:
"""Generate cache key from function name and arguments."""
# Create deterministic key from function and arguments
key_parts = [
func.__module__,
func.__name__,
str(args),
str(sorted(kwargs.items()))
]
key_string = ':'.join(key_parts)
key_hash = hashlib.md5(key_string.encode()).hexdigest()
return f"cache:{func.__name__}:{key_hash}"
def __call__(self, func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs):
# Generate cache key
cache_key = self.cache_key(func, *args, **kwargs)
# Try to get from cache
cached = self.redis.get(cache_key)
if cached:
print(f"Cache HIT: {cache_key}")
return json.loads(cached)
# Cache miss - execute function
print(f"Cache MISS: {cache_key}")
result = func(*args, **kwargs)
# Store in cache
self.redis.setex(
cache_key,
self.ttl,
json.dumps(result)
)
return result
# Add cache invalidation method
def invalidate(*args, **kwargs):
cache_key = self.cache_key(func, *args, **kwargs)
self.redis.delete(cache_key)
wrapper.invalidate = invalidate
return wrapper
Usage
redis = Redis(host='localhost', port=6379, db=0) cache = CacheDecorator(redis, ttl=300)
@cache def get_user_profile(user_id: int) -> dict: """Fetch user profile from database.""" print(f"Fetching user {user_id} from database...") # Simulate database query time.sleep(1) return { 'id': user_id, 'name': 'John Doe', 'email': 'john@example.com' }
First call - cache miss
profile = get_user_profile(123) # Takes 1 second
Second call - cache hit
profile = get_user_profile(123) # Instant
Invalidate cache
get_user_profile.invalidate(123)
- Multi-Level Cache
interface CacheLevel {
get(key: string): Promise
; set(key: string, value: any, ttl?: number): Promise ; delete(key: string): Promise ; }
class MemoryCache implements CacheLevel {
private cache = new Map
async get(key: string): Promise
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.value;
}
async set(key: string, value: any, ttl: number = 60): Promise
async delete(key: string): Promise
clear(): void { this.cache.clear(); } }
class RedisCache implements CacheLevel { constructor(private redis: Redis) {}
async get(key: string): Promise
async set(key: string, value: any, ttl: number = 3600): Promise
async delete(key: string): Promise
class MultiLevelCache { private levels: CacheLevel[];
constructor(levels: CacheLevel[]) { this.levels = levels; // Ordered from fastest to slowest }
async get
if (value !== null) {
// Backfill faster caches
for (let j = 0; j < i; j++) {
await this.levels[j].set(key, value);
}
return value as T;
}
}
return null;
}
async set(key: string, value: any, ttl?: number): Promise
async delete(key: string): Promise
// Usage const cache = new MultiLevelCache([ new MemoryCache(), new RedisCache(redis) ]);
// Get from fastest available cache const data = await cache.get('user:123');
// Set in all caches await cache.set('user:123', userData, 3600);
- Cache Invalidation Strategies class CacheInvalidation { constructor(private cache: CacheService) {}
/*
* Time-based invalidation (TTL)
/
async setWithTTL(key: string, value: any, seconds: number): Promise
/*
* Tag-based invalidation
/
async setWithTags(
key: string,
value: any,
tags: string[]
): Promise
// Store tag associations
for (const tag of tags) {
await this.cache.redis.sadd(`tag:${tag}`, key);
}
}
async invalidateByTag(tag: string): Promisetag:${tag});
if (keys.length === 0) return 0;
// Delete all keys
await Promise.all(
keys.map(key => this.cache.delete(key))
);
// Delete tag set
await this.cache.redis.del(`tag:${tag}`);
return keys.length;
}
/*
* Event-based invalidation
/
async invalidateOnEvent(
entity: string,
id: string,
event: 'create' | 'update' | 'delete'
): Promise${entity}:${id},
${entity}:${id}:*,
${entity}:list:*,
${entity}:count
];
for (const pattern of patterns) {
await this.cache.deletePattern(pattern);
}
}
/*
* Version-based invalidation
/
async setVersioned(
key: string,
value: any,
version: number
): Promise${key}:v${version};
await this.cache.set(versionedKey, value);
await this.cache.set(${key}:version, version);
}
async getVersioned(key: string): Promise${key}:version);
if (!version) return null;
return await this.cache.get(`${key}:v${version}`);
} }
- HTTP Caching Headers import express from 'express';
const app = express();
// Cache-Control middleware function cacheControl(maxAge: number, options: { private?: boolean; noStore?: boolean; noCache?: boolean; mustRevalidate?: boolean; staleWhileRevalidate?: number; } = {}) { return (req: express.Request, res: express.Response, next: express.NextFunction) => { const directives: string[] = [];
if (options.noStore) {
directives.push('no-store');
} else if (options.noCache) {
directives.push('no-cache');
} else {
directives.push(options.private ? 'private' : 'public');
directives.push(`max-age=${maxAge}`);
if (options.staleWhileRevalidate) {
directives.push(`stale-while-revalidate=${options.staleWhileRevalidate}`);
}
}
if (options.mustRevalidate) {
directives.push('must-revalidate');
}
res.setHeader('Cache-Control', directives.join(', '));
next();
}; }
// Static assets - long cache app.use('/static', cacheControl(31536000), express.static('public'));
// API - short cache with revalidation app.get('/api/data', cacheControl(60, { staleWhileRevalidate: 300 }), (req, res) => { res.json({ data: 'cached for 60s' }); } );
// Dynamic content - no cache app.get('/api/user/profile', cacheControl(0, { private: true, noCache: true }), (req, res) => { res.json({ user: 'always fresh' }); } );
// ETag support app.get('/api/resource/:id', async (req, res) => { const resource = await getResource(req.params.id); const etag = generateETag(resource);
res.setHeader('ETag', etag);
// Check if client has current version if (req.headers['if-none-match'] === etag) { return res.status(304).end(); }
res.json(resource); });
function generateETag(data: any): string { return require('crypto') .createHash('md5') .update(JSON.stringify(data)) .digest('hex'); }
Best Practices ✅ DO Set appropriate TTL values Implement cache warming for critical data Use cache-aside pattern for reads Monitor cache hit rates Implement graceful degradation on cache failure Use compression for large cached values Namespace cache keys properly Implement cache stampede prevention Use consistent hashing for distributed caching Monitor cache memory usage ❌ DON'T Cache everything indiscriminately Use caching as a fix for poor database design Store sensitive data without encryption Forget to handle cache misses Set TTL too long for frequently changing data Ignore cache invalidation strategies Cache without monitoring Store large objects without consideration Cache Strategies Strategy Description Use Case Cache-Aside Application checks cache, loads from DB on miss General purpose Write-Through Write to cache and DB simultaneously Strong consistency needed Write-Behind Write to cache, async write to DB High write throughput Refresh-Ahead Proactively refresh before expiry Predictable access patterns Read-Through Cache loads from DB automatically Simplified code Resources Redis Documentation Cache-Control Headers Caching Best Practices