Builders Academy
Études de Cas System Design
3 of 3

Chapitre 2 · Leçon 1

Système de Notifications

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é.

Architecture du système de notifications

snippet.typescript

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}
Idempotency keys — éviter les duplications

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.

System Design — synthèse

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.

Topics and skills covered

  • System Design
  • Notifications
  • Fan-out
  • Push
  • WebSocket