Caching Strategies
Problème
Les requêtes répétées à la DB ou à des APIs externes ralentissent l'app. Soit pas de cache, soit cache sans stratégie d'invalidation (données stale).
Solution
Stratégies de cache par use case + invalidation explicite + cache multi-niveau.
1. Les 4 patterns de cache
Cache-aside (lazy loading)
// Le plus courant — l'app gère le cache
async function getUser(id: string) {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.query.users.findFirst({ where: eq(users.id, id) });
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 300); // 5 min TTL
return user;
}
- Pro : simple, resilient (si cache down, on lit la DB)
- Con : cold start (première requête lente), données potentiellement stale
Write-through
// Écrit dans le cache et la DB en même temps
async function updateUser(id: string, data: UserUpdate) {
const user = await db.update(users).set(data).where(eq(users.id, id)).returning();
await redis.set(`user:${id}`, JSON.stringify(user[0]), 'EX', 300);
return user[0];
}
- Pro : cache toujours à jour
- Con : latence d'écriture plus élevée
Write-behind (write-back)
// Écrit dans le cache d'abord, DB plus tard (async)
async function incrementView(id: string) {
await redis.incr(`views:${id}`);
// Flush vers DB en batch toutes les 60s
}
- Pro : écritures ultra rapides
- Con : risque de perte de données si cache crash avant flush
Refresh-ahead
// Pré-chauffer le cache avant expiration
async function warmCache(id: string) {
const ttl = await redis.ttl(`user:${id}`);
if (ttl < 60) { // expire dans < 1 min
const user = await db.query.users.findFirst({ where: eq(users.id, id) });
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 300);
}
}
- Pro : pas de cold start
- Con : complexité, surcharge si peu de lectures
2. Multi-niveau
Request → CDN (static) → Browser cache → App cache (Redis) → DB
| Niveau | TTL | Contenu |
|---|---|---|
| CDN (Cloudflare, Vercel) | 1h-24h | Static assets, pages SSR, images |
| Browser | 1min-1h | Cache-Control: public, max-age=300 |
| Redis (app) | 30s-5min | DB queries, API responses, sessions |
| In-memory (process) | 1s-30s | Hot data, computed values |
3. Invalidation — le problème n°1
// Invalidation explicite (recommandé)
async function deleteUser(id: string) {
await db.delete(users).where(eq(users.id, id));
await redis.del(`user:${id}`);
await redis.del(`user:list`); // invalidate list cache too
}
// TTL court (sécurité)
// Si oubli d'invalidation, le TTL garantit la fraîcheur maximale
await redis.set(key, value, 'EX', 60); // max 1 min stale
// Cache-aside + TTL court = bon défaut
Règle d'or : invalider au moment de l'écriture + TTL court en filet de sécurité.
4. Redis — patterns courants
// Session store
await redis.set(`session:${sessionId}`, JSON.stringify(session), 'EX', 900); // 15 min
// Rate limiting (sliding window)
const key = `rate:${userId}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60);
// Pub/sub pour invalidation multi-instance
redis.subscribe('cache:invalidate', (msg) => {
const { key } = JSON.parse(msg);
localCache.delete(key);
});
5. CDN — cache headers
// Static assets — long TTL + immutable
app.use('/static', express.static('public', {
maxAge: '1y',
immutable: true
}));
// API responses — pas de cache par défaut, sauf endpoints spécifiques
app.get('/api/v1/products', (req, res) => {
res.set('Cache-Control', 'public, max-age=60, s-maxage=300');
res.set('Vary', 'Accept-Encoding');
// s-maxage = TTL pour le CDN (plus long que le browser)
});
// SSR pages — stale-while-revalidate
res.set('Cache-Control', 'public, max-age=60, stale-while-revalidate=600');
6. Memoization (in-process)
// Pour les computations pures et coûteuses
const memoize = <T>(fn: (...args: any[]) => T, ttl = 30000) => {
const cache = new Map<string, { value: T; expires: number }>();
return (...args: any[]): T => {
const key = JSON.stringify(args);
const hit = cache.get(key);
if (hit && hit.expires > Date.now()) return hit.value;
const value = fn(...args);
cache.set(key, { value, expires: Date.now() + ttl });
return value;
};
};
const computeScore = memoize((userId: string) => expensiveComputation(userId));
Anti-patterns
- Cache sans TTL (données stale à vie)
- Cache sans invalidation à l'écriture
- Tout cacher (cache de données qui changent tout le temps)
- Pas de
Varyheader sur les responses qui varient par content-type - Redis sans connection pooling
- Cache sur des données utilisateur sans clé par utilisateur
Références
- [[KNOW-PAT-227]] — Database Schema Design (indexing = première optimisation)
- [[KNOW-PAT-228]] — API REST Design (cache headers, ETags)
- [[KNOW-REF-014]] — System Design Primer (caching à grande échelle)