Patterns
Stratégies de cache Redis (balance, stats, webhooks)
TTL court + invalidation event-driven sur webhooks
Redis est utilisé chez Scell.io pour 4 cas. (1) Cache balance tenant : la balance change après chaque facture/recharge, TTL court (60s) + invalidation immédiate sur webhook `balance.updated`. (2) Cache stats agrégées : nombre de factures par mois, totaux TVA, top clients ; calcul coûteux, TTL 5 min, invalidation sur `invoice.created` / `invoice.cancelled`. (3) Idempotency keys (24h TTL, voir pattern dédié). (4) Rate limiting : sliding window avec `INCR + EXPIRE`. Pattern critique : NE JAMAIS cacher une donnée fiscale sans la chaîne d'invalidation correspondante. Le cache balance qui sert un montant obsolète provoque des doubles débits ou des refus de transaction. La règle `cache → webhook → invalidate` doit être documentée et testée. Anti-pattern : cache write-through vers Redis depuis l'application (incohérence si le service crash entre DB et Redis) ; préférer cache-aside (lecture cache miss → DB → cache write) avec invalidation event-driven sur webhook.
À retenir
- Balance tenant : TTL 60s + invalidation sur webhook balance.updated
- Stats agrégées : TTL 5min + invalidation sur invoice.created/cancelled
- Idempotency keys : TTL 24h
- Rate limit : sliding window via INCR + EXPIRE
- Pattern : cache-aside, jamais write-through pour données fiscales
Exemple de code
import { createClient } from 'redis';
const redis = createClient();
// Cache-aside avec invalidation event-driven
async function getBalance(tenantId: string): Promise<number> {
const key = `balance:${tenantId}`;
const cached = await redis.get(key);
if (cached !== null) return Number(cached);
const fresh = await db.query('SELECT balance FROM tenants WHERE id = $1', [tenantId]);
await redis.set(key, String(fresh), { EX: 60 }); // 60s TTL safety net
return fresh;
}
// Webhook handler — invalidation immédiate
async function onBalanceUpdated(event: { tenant_id: string }) {
await redis.del(`balance:${event.tenant_id}`);
await redis.del(`stats:${event.tenant_id}:current_month`);
}
// Rate limit sliding window
async function checkRateLimit(apiKey: string, limit = 1000, windowSec = 60): Promise<boolean> {
const key = `rl:${apiKey}:${Math.floor(Date.now() / 1000 / windowSec)}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, windowSec);
return count <= limit;
}