What is caching?
Caching stores the result of expensive operations (I/O/CPU/network) closer to where it’s consumed so future requests are served faster and cheaper.
Layers
- Client/browser, CDN/edge, reverse proxy, application cache (Redis/memory), database/materialized views.
Where to place the cache
- Client, CDN/edge, proxy, application tier, database (materialized views/indices).
Invalidation
- TTL, write-through/back, cache-aside; versioned keys, ETag/If-None-Match.
Consistency
- Stale-while-revalidate, read-through; beware thundering herd → locks/adaptive TTL.
Code: cache-aside with Redis (Node.js)
// language-javascript
import Redis from 'ioredis'
const redis = new Redis(process.env.REDIS_URL)
async function getUser(userId) {
const key = `user:${userId}`
const cached = await redis.get(key)
if (cached) return JSON.parse(cached)
const user = await db.users.findById(userId) // expensive
await redis.set(key, JSON.stringify(user), 'EX', 60) // TTL 60s
return user
}
Code: HTTP caching headers
# language-nginx
location /static/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
location /api/resource {
add_header Cache-Control "public, max-age=60, stale-while-revalidate=120";
proxy_pass http://app;
}
Stampede protection (single-flight)
// language-typescript
const inflight = new Map<string, Promise<any>>()
export async function getWithSingleFlight(key: string, fetcher: () => Promise<any>) {
const cached = await redis.get(key)
if (cached) return JSON.parse(cached)
if (inflight.has(key)) return inflight.get(key)
const p = (async () => {
try {
const val = await fetcher()
await redis.setex(key, 60, JSON.stringify(val))
return val
} finally {
inflight.delete(key)
}
})()
inflight.set(key, p)
return p
}
Write-through vs cache-aside
- Cache-aside: application reads DB and populates cache. Simple, but stale if writes bypass.
- Write-through: writes go through cache which updates DB. Lower staleness, more coupling.
- Write-back: write to cache, flush to DB asynchronously; higher risk, great for high write rates with durability safeguards.
Versioned keys for busting
// language-javascript
function userKey(id, v) { return `user:${id}:v${v}` }
// Bump version on write to bust all readers atomically
async function updateUser(id, patch) {
const v = await redis.incr(`user:${id}:ver`)
const data = await db.updateUser(id, patch)
await redis.set(userKey(id, v), JSON.stringify(data), 'EX', 300)
}
Materialized views (DB)
-- language-sql
CREATE MATERIALIZED VIEW top_articles AS
SELECT article_id, count(*) AS views
FROM page_views
GROUP BY article_id;
-- Refresh off-peak or via CDC triggers
REFRESH MATERIALIZED VIEW CONCURRENTLY top_articles;
CDN + app cache layering
- Public immutable assets: year-long TTL with content hashing.
- API GETs: CDN s-maxage with SWR; app cache 30–120s TTL.
- Personalized data: short TTL + ETag validators.
Metrics and alerts
- Hit ratio per tier, origin offload, p95 fetch time, stampede rate.
- Cache key cardinality (avoid explosive dimensions), memory usage, eviction rate.