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<T>(key: string): Promise<T | null> {
try {
const value = await this.redis.get(key);
if (!value) return null;
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<boolean> {
try {
const ttl = options.ttl || this.defaultTTL;
const serialized = JSON.stringify(value);
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): Promise<boolean> {
try {
await this.redis.del(key);
return true;
} catch (error) {
console.error(`Cache delete error for key ${key}:`, error);
return false;
}
}
/**
* Delete multiple keys by pattern
*/
async deletePattern(pattern: string): Promise<number> {
try {
const keys = await this.redis.keys(pattern);
if (keys.length === 0) return 0;
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<T>(
key: string,
fetchFn: () => Promise<T>,
options: CacheOptions = {},
): Promise<T> {
// Try to get from cache
const cached = await this.get<T>(key);
if (cached !== null) {
return cached;
}
// Fetch and cache
const value = await fetchFn();
await this.set(key, value, options);
return value;
}
/**
* Implement cache-aside pattern with stale-while-revalidate
*/
async getStaleWhileRevalidate<T>(
key: string,
fetchFn: () => Promise<T>,
options: {
ttl: number;
staleTime: number;
},
): Promise<T> {
const cacheKey = `cache:${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<number> {
const count = await this.redis.incr(key);
if (count === 1 && ttl) {
await this.redis.expire(key, ttl);
}
return count;
}
/**
* Check if key exists
*/
async exists(key: string): Promise<boolean> {
const result = await this.redis.exists(key);
return result === 1;
}
/**
* Get remaining TTL
*/
async ttl(key: string): Promise<number> {
return await this.redis.ttl(key);
}
/**
* Close connection
*/
async disconnect(): Promise<void> {
await this.redis.quit();
}
}
// 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 },
);
2. 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)
3. Multi-Level Cache
interface CacheLevel {
get(key: string): Promise<any>;
set(key: string, value: any, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
}
class MemoryCache implements CacheLevel {
private cache = new Map<string, { value: any; expiry: number }>();
async get(key: string): Promise<any> {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.value;
}
async set(key: string, value: any, ttl: number = 60): Promise<void> {
this.cache.set(key, {
value,
expiry: Date.now() + ttl * 1000,
});
}
async delete(key: string): Promise<void> {
this.cache.delete(key);
}
clear(): void {
this.cache.clear();
}
}
class RedisCache implements CacheLevel {
constructor(private redis: Redis) {}
async get(key: string): Promise<any> {
const value = await this.redis.get(key);
return value ? JSON.parse(value) : null;
}
async set(key: string, value: any, ttl: number = 3600): Promise<void> {
await this.redis.setex(key, ttl, JSON.stringify(value));
}
async delete(key: string): Promise<void> {
await this.redis.del(key);
}
}
class MultiLevelCache {
private levels: CacheLevel[];
constructor(levels: CacheLevel[]) {
this.levels = levels; // Ordered from fastest to slowest
}
async get<T>(key: string): Promise<T | null> {
for (let i = 0; i < this.levels.length; i++) {
const value = await this.levels[i].get(key);
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<void> {
// Set in all cache levels
await Promise.all(this.levels.map((level) => level.set(key, value, ttl)));
}
async delete(key: string): Promise<void> {
await Promise.all(this.levels.map((level) => level.delete(key)));
}
}
// 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);
4. Cache Invalidation Strategies
class CacheInvalidation {
constructor(private cache: CacheService) {}
/**
* Time-based invalidation (TTL)
*/
async setWithTTL(key: string, value: any, seconds: number): Promise<void> {
await this.cache.set(key, value, { ttl: seconds });
}
/**
* Tag-based invalidation
*/
async setWithTags(key: string, value: any, tags: string[]): Promise<void> {
// Store value
await this.cache.set(key, value);
// Store tag associations
for (const tag of tags) {
await this.cache.redis.sadd(`tag:${tag}`, key);
}
}
async invalidateByTag(tag: string): Promise<number> {
// Get all keys with this tag
const keys = await this.cache.redis.smembers(`tag:${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<void> {
const patterns = [
`${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<void> {
const versionedKey = `${key}:v${version}`;
await this.cache.set(versionedKey, value);
await this.cache.set(`${key}:version`, version);
}
async getVersioned(key: string): Promise<any> {
const version = await this.cache.get<number>(`${key}:version`);
if (!version) return null;
return await this.cache.get(`${key}:v${version}`);
}
}
5. 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 |