Cache Project: Build a Multi-Layer Caching System from Scratch
In this tutorial, you will learn about Cache Project: Build a Multi. We cover key concepts, practical examples, and best practices to help you master this topic.
This project brings together everything you have learned about Caching. You will build a multi-layer caching system for a product catalog API that uses an in-memory L1 cache, a Redis L2 cache, CDN-like cache headers, and event-driven cache invalidation.
flowchart TB
Client[Client] --> LB[Load Balancer]
LB --> Nginx[Nginx CDN Simulator]
Nginx --> App1[App Instance 1]
Nginx --> App2[App Instance 2]
App1 --> L1[L1: In-Memory Cache]
App2 --> L1
L1 --> L2[L2: Redis Cache]
L2 --> DB[(PostgreSQL)]
App1 --> Invalidator[Cache Invalidator]
App2 --> Invalidator
Invalidator --> Redis_PubSub[Redis Pub/Sub]
Redis_PubSub --> L1
Redis_PubSub --> L2
Project Requirements
Build the following components:
1. Multi-Layer Cache
class MultiLayerCache {
constructor() {
this.l1 = new LRU.LRUCache({ max: 500, maxAge: 30000 });
this.l2 = redisClient;
this.stats = { l1Hits: 0, l2Hits: 0, misses: 0 };
}
async get(key, fetchFn) {
// L1 check
const l1Result = this.l1.get(key);
if (l1Result !== undefined) {
this.stats.l1Hits++;
return l1Result;
}
// L2 check
const l2Result = await this.l2.get(key);
if (l2Result) {
this.stats.l2Hits++;
const parsed = JSON.parse(l2Result);
this.l1.set(key, parsed);
return parsed;
}
// Miss: fetch from source
this.stats.misses++;
const data = await fetchFn();
if (data) {
await this.l2.setEx(key, 3600, JSON.stringify(data));
this.l1.set(key, data);
}
return data;
}
async invalidate(key) {
this.l1.delete(key);
await this.l2.del(key);
}
stats() {
const total = this.stats.l1Hits + this.stats.l2Hits + this.stats.misses;
return {
l1HitRate: total > 0 ? (this.stats.l1Hits / total * 100).toFixed(1) + '%' : '0%',
l2HitRate: total > 0 ? (this.stats.l2Hits / total * 100).toFixed(1) + '%' : '0%',
overallHitRate: total > 0 ? ((this.stats.l1Hits + this.stats.l2Hits) / total * 100).toFixed(1) + '%' : '0%',
totalRequests: total
};
}
}
Expected output:
L1 hit rate: 65.3%, L2 hit rate: 30.1%, overall: 95.4%. Total requests: 10000.
2. Event-Driven Invalidation
class ProjectInvalidator {
constructor(redisClient) {
this.redis = redisClient;
this.subscriber = redisClient.duplicate();
this.listeners = new Map();
}
async connect() {
await this.subscriber.subscribe('cache:invalidate');
this.subscriber.on('message', (channel, message) => {
const { key, pattern } = JSON.parse(message);
const handler = this.listeners.get(pattern || key);
if (handler) handler(key);
});
}
async invalidate(key) {
await this.redis.publish('cache:invalidate', JSON.stringify({ key }));
}
onInvalidate(pattern, handler) {
this.listeners.set(pattern, handler);
}
}
Expected output:
When a product is updated, the API calls invalidator.invalidate('product:123'). All nodes receive the invalidation event and clear their L1 and L2 caches.
3. CDN-Simulated Nginx Configuration
proxy_cache_path /tmp/nginx-cache levels=1:2 keys_zone=cdn_cache:10m max_size=500m;
server {
listen 8080;
location /api/ {
proxy_cache cdn_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 60s;
proxy_cache_valid 404 10s;
proxy_cache_use_stale error timeout updating;
add_header X-CDN-Cache $upstream_cache_status;
proxy_pass http://node_app:3000;
}
location /purge/ {
proxy_cache_purge PURGE from 127.0.0.1;
}
}
Expected output:
First request: X-CDN-Cache: MISS. Subsequent requests within 60s: X-CDN-Cache: HIT. PURGE requests clear the cache for a path.
4. Cache Warming on Startup
async function warmCache(api, popularIds) {
console.log(`Warming cache with ${popularIds.length} popular products...`);
const results = await Promise.allSettled(
popularIds.map(id =>
fetch(`${api}/products/${id}`).then(r => r.json())
)
);
const warmed = results.filter(r => r.status === 'fulfilled').length;
console.log(`Cache warmed: ${warmed}/${popularIds.length} products cached`);
}
Expected output:
Warming cache with 100 popular products...
Cache warmed: 100/100 products cached
Acceptance Criteria
- In-memory L1 cache serves hot keys in <1ms
- Redis L2 cache serves warm keys in <5ms
- Overall cache hit rate > 90% under sustained load
- Invalidation propagates to all instances within 100ms
- Cache warming completes within 5 seconds on startup
- CDN simulator shows MISS on first request, HIT on subsequent
- Power outage simulation: recovering 4 instances shows graceful degradation
Testing
const http = require('http');
async function runLoadTest() {
const products = Array.from({ length: 100 }, (_, i) => i + 1);
const results = { l1Hits: 0, l2Hits: 0, misses: 0, errors: 0, latencies: [] };
const promises = [];
for (let i = 0; i < 5000; i++) {
const id = products[Math.floor(Math.random() * products.length)];
promises.push(
measureRequest(`http://localhost:3000/api/products/${id}`)
.then(r => {
results[r.cacheType === 'l1' ? 'l1Hits' : r.cacheType === 'l2' ? 'l2Hits' : 'misses']++;
results.latencies.push(r.latency);
})
.catch(() => results.errors++)
);
}
await Promise.all(promises);
const total = results.l1Hits + results.l2Hits + results.misses;
const avgLat = results.latencies.reduce((a, b) => a + b, 0) / results.latencies.length;
console.log(`Results: ${total} requests`);
console.log(`L1 Hits: ${results.l1Hits} (${(results.l1Hits/total*100).toFixed(1)}%)`);
console.log(`L2 Hits: ${results.l2Hits} (${(results.l2Hits/total*100).toFixed(1)}%)`);
console.log(`Misses: ${results.misses} (${(results.misses/total*100).toFixed(1)}%)`);
console.log(`Avg Latency: ${avgLat.toFixed(3)}ms`);
}
function measureRequest(url) {
const start = Date.now();
return fetch(url).then(res =>
res.json().then(() => ({
latency: Date.now() - start,
cacheType: res.headers.get('X-Cache-Layer') || 'miss'
}))
);
}
Expected output:
Results: 5000 requests
L1 Hits: 3254 (65.1%)
L2 Hits: 1520 (30.4%)
Misses: 226 (4.5%)
Avg Latency: 1.234ms
Common Mistakes
- Skipping cache warming — first requests after deploy are slow and may timeout under load.
- Not testing invalidation under concurrent writes — race conditions can leave stale data in the cache.
- Using the same TTL for all layers — L1 should have shorter TTL than L2.
- Forgetting to handle cache server failures — implement circuit breakers for L2.
- Not monitoring cache metrics during the project — you won't know if your cache is effective.
FAQ
Submission Checklist
- Multi-layer cache (L1 + L2) implemented
- Cache warming on startup
- Event-driven invalidation with Redis pub/sub
- CDN simulation with Nginx
- Metrics endpoint exposing hit rates and latency
- Load test demonstrating >90% hit rate
- Graceful degradation when Redis is unavailable
- Cache stampede protection
What's Next
Congratulations on completing the caching strategies module. Continue to Backend Security to learn how to protect your backend from common vulnerabilities and attacks.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro