Explorer
KNOW-PAT-229

Caching Strategies — Cache-aside, write-through, CDN, Redis, invalidation

Domaine
performance
Type
pattern
Priorité
P1

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 Vary header 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)