The Caching Pyramid
Effective caching happens at multiple layers. Each layer serves different purposes and has different characteristics.
Browser Cache
โ
CDN Cache
โ
API Gateway Cache
โ
Application Cache
โ
Database CacheBrowser Caching
Cache-Control Headers
// Static assets - cache for 1 year
Cache-Control: public, max-age=31536000, immutable
// API responses - cache with revalidation
Cache-Control: private, max-age=0, must-revalidate
ETag: "abc123"Service Worker Caching
// Cache-first strategy for static assets
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cached) => {
return cached || fetch(event.request);
})
);
});CDN Caching
- Cache static assets globally
- Use cache keys wisely (URL, headers, cookies)
- Implement cache purging for updates
- Consider edge computing for dynamic content
// CloudFront cache behavior
{
"PathPattern": "/static/*",
"TTL": {
"DefaultTTL": 86400,
"MaxTTL": 31536000
},
"CacheKeyPolicy": "CachingOptimized"
}Application Caching
In-Memory Cache
// Node.js with node-cache
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 600 });
async function getUser(id) {
const cached = cache.get(`user:${id}`);
if (cached) return cached;
const user = await db.users.findById(id);
cache.set(`user:${id}`, user);
return user;
}Distributed Cache (Redis)
const redis = require('redis');
const client = redis.createClient();
async function getProduct(id) {
// Try cache first
const cached = await client.get(`product:${id}`);
if (cached) return JSON.parse(cached);
// Fetch from database
const product = await db.products.findById(id);
// Cache with expiration
await client.setEx(`product:${id}`, 3600, JSON.stringify(product));
return product;
}Cache Patterns
Cache-Aside (Lazy Loading)
// Application manages cache explicitly
async function getData(key) {
let data = await cache.get(key);
if (!data) {
data = await database.get(key);
await cache.set(key, data, TTL);
}
return data;
}Write-Through
// Write to cache and database together
async function saveData(key, value) {
await database.save(key, value);
await cache.set(key, value);
}Write-Behind
// Write to cache immediately, database asynchronously
async function saveData(key, value) {
await cache.set(key, value);
queue.add({ key, value }); // Process later
}Cache Invalidation
"There are only two hard things in Computer Science: cache invalidation and naming things."
Strategies
- TTL-based: Data expires after set time
- Event-based: Invalidate on data changes
- Version-based: Include version in cache key
// Event-based invalidation
async function updateProduct(id, data) {
await db.products.update(id, data);
await cache.delete(`product:${id}`);
await cache.delete(`products:list`); // Invalidate related caches
}Conclusion
Caching is essential for performance but adds complexity. Start simple, measure impact, and add sophistication only where needed.