Caching Strategies for Backend Applications
Cache-Aside (Lazy Loading)
The application manages the cache manually. Most common pattern.
class UserService {
constructor(
private userRepo: UserRepository,
private cache: RedisCache
) {}
async getUser(userId: string): Promise<User> {
const cacheKey = `user:${userId}`;
// 1. Check cache first
const cached = await this.cache.get<User>(cacheKey);
if (cached) return cached;
// 2. Cache miss - fetch from DB
const user = await this.userRepo.findById(userId);
if (!user) throw new UserNotFoundError(userId);
// 3. Store in cache with TTL
await this.cache.set(cacheKey, user, { ttl: 3600 }); // 1 hour
return user;
}
async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
const user = await this.userRepo.update(userId, dto);
// Invalidate cache after update
await this.cache.delete(`user:${userId}`);
return user;
}
}
Write-Through Cache
Write to cache and DB simultaneously.
async setUser(user: User): Promise<void> {
const cacheKey = `user:${user.id}`;
// Write to both simultaneously
await Promise.all([
this.userRepo.save(user),
this.cache.set(cacheKey, user, { ttl: 3600 }),
]);
}
Redis Cache Implementation
import { createClient } from 'redis';
class RedisCache {
private client = createClient({ url: process.env.REDIS_URL });
async get<T>(key: string): Promise<T | null> {
const data = await this.client.get(key);
if (!data) return null;
return JSON.parse(data) as T;
}
async set<T>(key: string, value: T, options: { ttl?: number } = {}): Promise<void> {
const serialized = JSON.stringify(value);
if (options.ttl) {
await this.client.setEx(key, options.ttl, serialized);
} else {
await this.client.set(key, serialized);
}
}
async delete(key: string): Promise<void> {
await this.client.del(key);
}
async deletePattern(pattern: string): Promise<void> {
const keys = await this.client.keys(pattern);
if (keys.length > 0) {
await this.client.del(keys);
}
}
async getOrSet<T>(key: string, factory: () => Promise<T>, ttl = 3600): Promise<T> {
const cached = await this.get<T>(key);
if (cached !== null) return cached;
const value = await factory();
await this.set(key, value, { ttl });
return value;
}
}
Cache Invalidation Strategies
// Tag-based invalidation
class TaggedCache {
async setWithTags(key: string, value: unknown, tags: string[], ttl = 3600): Promise<void> {
await this.cache.set(key, value, { ttl });
// Store reverse mapping: tag -> keys
for (const tag of tags) {
await this.client.sAdd(`tag:${tag}`, key);
await this.client.expire(`tag:${tag}`, ttl);
}
}
async invalidateByTag(tag: string): Promise<void> {
const keys = await this.client.sMembers(`tag:${tag}`);
if (keys.length > 0) {
await this.client.del([...keys, `tag:${tag}`]);
}
}
}
// When user updates, invalidate related caches
await taggedCache.setWithTags(
`user:${userId}`,
user,
[`user:${userId}`, 'users:list'],
3600
);
await taggedCache.invalidateByTag(`user:${userId}`);
Cache Stampede Prevention
// Mutex lock to prevent multiple simultaneous rebuilds
async getWithLock<T>(key: string, factory: () => Promise<T>): Promise<T> {
const cached = await this.cache.get<T>(key);
if (cached !== null) return cached;
const lockKey = `lock:${key}`;
const lockAcquired = await this.client.set(lockKey, '1', { NX: true, EX: 5 });
if (!lockAcquired) {
// Another process is rebuilding, wait and retry
await new Promise(resolve => setTimeout(resolve, 100));
return this.getWithLock(key, factory);
}
try {
const value = await factory();
await this.cache.set(key, value, { ttl: 3600 });
return value;
} finally {
await this.client.del(lockKey);
}
}
Cache Hit Rate Monitoring
class MonitoredCache extends RedisCache {
private hits = 0;
private misses = 0;
async get<T>(key: string): Promise<T | null> {
const result = await super.get<T>(key);
if (result !== null) {
this.hits++;
metrics.increment('cache.hit', { key: key.split(':')[0] });
} else {
this.misses++;
metrics.increment('cache.miss', { key: key.split(':')[0] });
}
return result;
}
get hitRate(): number {
const total = this.hits + this.misses;
return total > 0 ? this.hits / total : 0;
}
}
Aim for 95%+ cache hit rate on frequently accessed data. Monitor and tune TTLs accordingly.