Chapitre 2 · Leçon 1
Push, email, in-app — 10M notifications/jour
Concevoir un système capable d'envoyer 10 millions de notifications par jour, via email, push mobile et in-app. Ce problème classique de System Design teste la compréhension des queues, du fan-out et de la scalabilité.
snippet.typescript
1/*
2 * SYSTÈME DE NOTIFICATIONS — Architecture
3 *
4 * REQUIREMENTS :
5 * - 10M notifications/jour (116/s en moyenne, 1000/s au pic)
6 * - Canaux : email, push mobile (iOS/Android), in-app (WebSocket)
7 * - Templates configurables
8 * - Préférences utilisateur (opt-out par canal et par type)
9 * - Garantie de livraison (at-least-once)
10 * - Déduplication (si le même event arrive 2 fois)
11 *
12 * ─────────────────────────────────────────────────────────────
13 * FLUX : Déclencheur → Notification Service → Channels
14 * ─────────────────────────────────────────────────────────────
15 *
16 * 1. EVENT PRODUCER (n'importe quel service)
17 * UserService → publish("tutorial.completed", { userId, tutorialId })
18 * ContentSvc → publish("new.tutorial", { categorySlug, tutorialId })
19 *
20 * 2. NOTIFICATION SERVICE (cœur du système)
21 * - Consomme les events
22 * - Applique les règles (qui notifier ? via quel canal ?)
23 * - Récupère les préférences utilisateur (cache Redis)
24 * - Fan-out : 1 event → N notifications
25 *
26 * 3. CHANNEL WORKERS (spécialisés par canal)
27 * EmailWorker → Sendgrid / SES
28 * PushWorker → FCM (Android) + APNs (iOS)
29 * InAppWorker → WebSocket via Socket.io
30 *
31 * ─────────────────────────────────────────────────────────────
32 * GESTION DU FAN-OUT
33 * ─────────────────────────────────────────────────────────────
34 *
35 * Event : "new.tutorial" dans la catégorie "web"
36 * → Trouver tous les users qui suivent "web" : 50 000 users
37 * → 50 000 notifications à créer et envoyer
38 *
39 * Problème : parcourir 50K users de façon synchrone = trop lent
40 *
41 * Solution — Fan-out par batch :
42 */
43
44async function fanOutNotification(event: TutorialPublishedEvent) {
45 const batchSize = 1000;
46 let cursor = 0;
47
48 while (true) {
49 // Récupérer un batch d'abonnés
50 const subscribers = await db.userCategorySubscription.findMany({
51 where: { categorySlug: event.categorySlug },
52 select: { userId: true },
53 skip: cursor,
54 take: batchSize,
55 });
56
57 if (subscribers.length === 0) break;
58
59 // Créer les notifications en batch
60 await db.notification.createMany({
61 data: subscribers.map(({ userId }) => ({
62 userId,
63 type: "NEW_TUTORIAL",
64 payload: { tutorialId: event.tutorialId, title: event.title },
65 status: "PENDING",
66 })),
67 });
68
69 // Publier dans la queue pour le dispatch
70 await notificationQueue.addBulk(
71 subscribers.map(({ userId }) => ({
72 name: "dispatch-notification",
73 data: { userId, tutorialId: event.tutorialId },
74 opts: { attempts: 3, backoff: { type: "exponential", delay: 1000 } },
75 }))
76 );
77
78 cursor += batchSize;
79 if (subscribers.length < batchSize) break;
80 }
81}
82
83// Worker de dispatch — envoie via le bon canal selon les préférences
84async function dispatchNotification(job: Job) {
85 const { userId, tutorialId } = job.data;
86
87 // Préférences en cache Redis (invalidées quand l'utilisateur les change)
88 const prefs = await getOrCacheUserPreferences(userId);
89
90 const tasks = [];
91
92 if (prefs.emailEnabled && prefs.newTutorialEmail) {
93 tasks.push(emailQueue.add("new-tutorial", { userId, tutorialId }));
94 }
95 if (prefs.pushEnabled && prefs.newTutorialPush) {
96 tasks.push(pushQueue.add("new-tutorial", { userId, tutorialId }));
97 }
98 // In-app : toujours livré, stocké en DB + WebSocket si connecté
99 tasks.push(inAppQueue.add("new-tutorial", { userId, tutorialId }));
100
101 await Promise.allSettled(tasks);
102}Pour garantir qu'une notification n'est envoyée qu'une seule fois même en cas de retry, utilise une clé d'idempotence unique : hash(userId + eventType + eventId). Si la clé existe dans Redis → skip.
Un bon System Design commence toujours par les requirements, pas par les technologies. Les compromis (trade-offs) sont inévitables — cohérence vs disponibilité, performance vs coût, simplicité vs scalabilité. L'ingénieur qui sait articuler et justifier ses choix est plus précieux que celui qui connaît le dernier framework à la mode.