Caching Strategies
Overview
Q01 Core APIs benefit from multi-layer caching to reduce latency and database load. Implement caching at client, CDN, application, and database levels for optimal performance.
Caching Layers:
- Browser Cache - Static assets, API responses
- CDN Cache - Media files, public content
- Application Cache - Redis/Memcached for API responses
- Database Cache - Query result cache
Benefits:
- ✅ Reduced latency (faster response times)
- ✅ Lower database load
- ✅ Better scalability
- ✅ Cost savings (fewer database queries)
Cache Invalidation
Event-Driven Invalidation
Use Outbox events to invalidate cache:
// cache-invalidator subscriber
class CacheInvalidator {
async handleEvent(event) {
const keys = this.getCacheKeys(event);
for (const key of keys) {
await redis.del(key);
console.log(`Invalidated: ${key}`);
}
}
getCacheKeys(event) {
const { AGGREGATE_TYPE, AGGREGATE_ID, PAYLOAD } = event;
const keys = [];
// Specific record
keys.push(`${AGGREGATE_TYPE}:${AGGREGATE_ID}`);
// List caches
keys.push(`${AGGREGATE_TYPE}:list:*`);
// Related caches
if (AGGREGATE_TYPE === 'PRD' && PAYLOAD.XPRD05) {
keys.push(`CAT:${PAYLOAD.XPRD05}:products`);
}
return keys;
}
}
Client-Side Caching
Browser Cache Headers
// CoreService sets cache headers
app.get('/api/v4/core/:dim/:id', (req, res) => {
// Public data - cache 5 minutes
res.set('Cache-Control', 'public, max-age=300');
// Private data - cache only in browser
res.set('Cache-Control', 'private, max-age=60');
// No cache for sensitive data
res.set('Cache-Control', 'no-store');
});
JavaScript Cache Implementation
class CachedAPIClient {
constructor(ttl = 60000) {
this.cache = new Map();
this.ttl = ttl;
}
async get(url) {
const cached = this.cache.get(url);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const data = await fetch(url).then(r => r.json());
this.cache.set(url, {
data,
timestamp: Date.now()
});
return data;
}
invalidate(url) {
this.cache.delete(url);
}
clear() {
this.cache.clear();
}
}
Application-Level Caching
Redis Cache
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
class APICache {
async get(key) {
const cached = await redis.get(key);
return cached ? JSON.parse(cached) : null;
}
async set(key, value, ttl = 300) {
await redis.set(key, JSON.stringify(value), 'EX', ttl);
}
async del(key) {
await redis.del(key);
}
async delPattern(pattern) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
}
// Usage
const cache = new APICache();
async function getProduct(id) {
const cacheKey = `product:${id}`;
// Check cache
let product = await cache.get(cacheKey);
if (product) return product;
// Fetch from API
product = await fetchProductFromAPI(id);
// Store in cache (5 minutes)
await cache.set(cacheKey, product, 300);
return product;
}
Cache-Aside Pattern
async function getProducts(filters) {
const cacheKey = `products:${JSON.stringify(filters)}`;
// 1. Check cache
let products = await cache.get(cacheKey);
if (products) {
console.log('Cache hit');
return products;
}
// 2. Cache miss - fetch from API
console.log('Cache miss');
products = await api.getProducts(filters);
// 3. Store in cache
await cache.set(cacheKey, products, 300);
return products;
}
CDN Caching
CloudFront Configuration
// Cache media files
app.get('/api/v4/media/:dim/:id/:field', (req, res) => {
// Long cache for immutable media
res.set('Cache-Control', 'public, max-age=31536000, immutable');
res.set('ETag', generateETag(file));
res.sendFile(filePath);
});
Cache Patterns
Read-Through Cache
class ReadThroughCache {
constructor(loader, ttl = 300) {
this.loader = loader;
this.cache = new Map();
this.ttl = ttl;
}
async get(key) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl * 1000) {
return cached.value;
}
// Load from source
const value = await this.loader(key);
this.cache.set(key, {
value,
timestamp: Date.now()
});
return value;
}
}
// Usage
const productCache = new ReadThroughCache(
async (id) => await api.getProduct(id),
300
);
const product = await productCache.get('123');
Write-Through Cache
async function updateProduct(id, data) {
// 1. Update database via API
await api.updateProduct(id, data);
// 2. Update cache immediately
await cache.set(`product:${id}`, data, 300);
return data;
}
Write-Behind Cache
class WriteBehindCache {
constructor() {
this.queue = [];
this.flushInterval = 5000; // 5 seconds
setInterval(() => this.flush(), this.flushInterval);
}
async set(key, value) {
// Update cache immediately
await cache.set(key, value);
// Queue for database write
this.queue.push({ key, value });
}
async flush() {
if (this.queue.length === 0) return;
const batch = this.queue.splice(0, 100);
for (const { key, value } of batch) {
await api.update(key, value);
}
}
}
Best Practices
✅ DO:
Set appropriate TTL:
// ✅ Good - different TTL for different data
await cache.set('static-config', data, 3600); // 1 hour
await cache.set('product-list', data, 300); // 5 minutes
await cache.set('user-session', data, 1800); // 30 minutes
Invalidate on writes:
// ✅ Good - invalidate related caches
async function updateProduct(id, data) {
await api.updateProduct(id, data);
await cache.del(`product:${id}`);
await cache.delPattern(`products:list:*`);
await cache.delPattern(`category:${data.category}:*`);
}
Use cache keys wisely:
// ✅ Good - structured keys
const key = `${tenant}:${dimension}:${id}:${version}`;
❌ DON'T:
Don't cache everything:
// ❌ Bad - caching frequently changing data
await cache.set('current-time', Date.now(), 3600);
// ✅ Good - cache stable data
await cache.set('product-catalog', products, 300);
Don't forget to invalidate:
// ❌ Bad - stale data
await api.updateProduct(id, data);
// Cache not invalidated!
// ✅ Good - invalidate immediately
await api.updateProduct(id, data);
await cache.del(`product:${id}`);
Summary
- ✅ Multi-layer caching (browser, CDN, application, database)
- ✅ Event-driven cache invalidation
- ✅ Cache-aside pattern for reads
- ✅ Write-through for immediate consistency
- ✅ Appropriate TTL for different data types
- ✅ Structured cache keys
Key Takeaways:
- Cache at multiple layers for best performance
- Invalidate cache on writes using Outbox events
- Set appropriate TTL based on data volatility
- Use Redis for application-level caching
- Implement cache patterns (read-through, write-through)
- Monitor cache hit rates
Related Concepts
- Performance Optimization - Query tuning
- Outbox Subscribers - Cache invalidation events
- Advanced Topics - Overview