Design a Notification System

0/4 in this phase0/45 across the roadmap

📖 Concept

A notification system delivers messages to users across multiple channels: push notifications (mobile), email, SMS, in-app notifications, and webhooks.

Requirements

Functional: Multi-channel delivery (push, email, SMS, in-app), template management, user preferences (opt-in/out per channel), scheduling, priority levels Non-Functional: 10B notifications/day, low latency for critical alerts, at-least-once delivery, graceful handling of third-party failures

Architecture

Event-Driven Design

  1. Service emits event: order.shipped {userId, orderId, trackingUrl}
  2. Notification service receives event
  3. Looks up user preferences (channels opted in)
  4. Renders notification template per channel
  5. Routes to channel-specific workers (push/email/SMS)
  6. Workers deliver via third-party APIs (FCM, SendGrid, Twilio)

Priority Queues

  • Critical (P0): Security alerts, 2FA codes → immediate delivery
  • High (P1): Order confirmations → within seconds
  • Medium (P2): Social notifications → batched, within minutes
  • Low (P3): Marketing → scheduled, respect quiet hours

Deduplication

Same notification sent twice is worse than not at all. Use idempotency keys: hash(userId + eventType + eventId) → check before sending.

Interview tip: Notification systems are great for demonstrating queue architecture, fan-out, and third-party API integration. Show the multi-channel routing and priority handling.

💻 Code Example

codeTap to expand ⛶
1// ============================================
2// Notification System — Architecture
3// ============================================
4
5class NotificationService {
6 constructor(queue, db, templateEngine) {
7 this.queue = queue;
8 this.db = db;
9 this.templates = templateEngine;
10 this.channels = {
11 push: new PushWorker(),
12 email: new EmailWorker(),
13 sms: new SMSWorker(),
14 inApp: new InAppWorker(),
15 };
16 }
17
18 async processEvent(event) {
19 // 1. Get user preferences
20 const prefs = await this.db.getUserNotificationPrefs(event.userId);
21
22 // 2. Check deduplication
23 const dedupKey = `notif:\${event.userId}:\${event.type}:\${event.id}`;
24 const alreadySent = await this.db.cache.get(dedupKey);
25 if (alreadySent) return;
26
27 // 3. Route to enabled channels
28 const enabledChannels = this.getEnabledChannels(event.type, prefs);
29
30 for (const channel of enabledChannels) {
31 // 4. Render template for each channel
32 const content = this.templates.render(event.type, channel, event.data);
33
34 // 5. Queue with priority
35 await this.queue.publish(`notifications.\${channel}`, {
36 userId: event.userId,
37 channel,
38 content,
39 priority: this.getPriority(event.type),
40 dedupKey,
41 });
42 }
43
44 // 6. Mark as sent (dedup)
45 await this.db.cache.set(dedupKey, '1', 'EX', 86400);
46 }
47
48 getEnabledChannels(eventType, prefs) {
49 const defaults = {
50 'security.alert': ['push', 'email', 'sms'],
51 'order.shipped': ['push', 'email'],
52 'social.like': ['push', 'inApp'],
53 'marketing.promo': ['email'],
54 };
55 const defaultChannels = defaults[eventType] || ['inApp'];
56 return defaultChannels.filter(ch => prefs[ch] !== false);
57 }
58
59 getPriority(eventType) {
60 if (eventType.startsWith('security')) return 0; // Critical
61 if (eventType.startsWith('order')) return 1; // High
62 if (eventType.startsWith('social')) return 2; // Medium
63 return 3; // Low
64 }
65}
66
67class PushWorker {
68 async send(notification) {
69 console.log(`[Push] → User \${notification.userId}: \${notification.content.title}`);
70 // Call FCM/APNs API
71 }
72}
73class EmailWorker {
74 async send(notification) {
75 console.log(`[Email] → User \${notification.userId}: \${notification.content.subject}`);
76 }
77}
78class SMSWorker {
79 async send(notification) {
80 console.log(`[SMS] → User \${notification.userId}: \${notification.content.body}`);
81 }
82}
83class InAppWorker {
84 async send(notification) {
85 console.log(`[In-App] → User \${notification.userId}: \${notification.content.message}`);
86 }
87}
88
89console.log("Notification system architecture demonstrated.");

🏋️ Practice Exercise

  1. Full Notification Design: Design a notification system for an e-commerce platform. Include: push, email, SMS, in-app. Handle: user preferences, quiet hours, rate limiting, batching.

  2. Third-Party Failure: What happens when FCM (push) or SendGrid (email) goes down? Design retry logic, circuit breakers, and fallback channels.

  3. Notification Batching: A user gets 50 likes in 10 minutes. Instead of 50 push notifications, send one: "50 people liked your post." Design the batching logic.

  4. Global Notification: Send a notification to ALL 100M users (system maintenance announcement). Design the fan-out strategy.

⚠️ Common Mistakes

  • Not deduplicating notifications — without idempotency checks, retries or duplicate events cause users to receive the same notification multiple times.

  • Synchronous delivery — sending push/email/SMS synchronously blocks the main flow. Use queues for async delivery with per-channel workers.

  • Ignoring user preferences — sending notifications on opted-out channels is a legal/UX problem. Always check preferences before sending.

  • No rate limiting per user — a misbehaving service can trigger thousands of notifications to one user. Implement per-user rate limits.

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Design a Notification System