Patterns
Redis cache strategies (balance, stats, webhooks)
Short TTL + event-driven invalidation on webhooks
Redis is used at Scell.io for 4 cases. (1) Tenant balance cache: balance changes after each invoice/reload, short TTL (60s) + immediate invalidation on `balance.updated` webhook. (2) Aggregated stats cache: monthly invoice count, VAT totals, top clients; expensive computation, 5-min TTL, invalidation on `invoice.created` / `invoice.cancelled`. (3) Idempotency keys (24h TTL, see dedicated pattern). (4) Rate limiting: sliding window with `INCR + EXPIRE`. Critical pattern: NEVER cache fiscal data without the corresponding invalidation chain. A balance cache serving a stale amount causes double-debits or transaction refusals. The `cache → webhook → invalidate` rule must be documented and tested. Anti-pattern: write-through cache to Redis from the application (inconsistency if service crashes between DB and Redis); prefer cache-aside (read cache miss → DB → cache write) with event-driven invalidation on webhook.
Key facts
- Tenant balance: 60s TTL + invalidation on balance.updated webhook
- Aggregated stats: 5-min TTL + invalidation on invoice.created/cancelled
- Idempotency keys: 24h TTL
- Rate limit: sliding window via INCR + EXPIRE
- Pattern: cache-aside, never write-through for fiscal data
Code example
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;
}