Difficulty: Advanced
Design a notification system that supports push notifications, email, SMS, and in-app notifications for millions of users.
A notification system must handle multiple channels (push, email, SMS, in-app), respect user preferences, support prioritization, and scale to millions of notifications per day.
Core components: 1. Notification Service: Accepts notification requests, validates, and routes 2. User Preference Store: Per-user channel preferences and quiet hours 3. Priority Queue: Urgent (OTP) vs normal (marketing) notifications 4. Channel Adapters: Push (FCM/APNs), Email (SES), SMS (Twilio), In-app (WebSocket) 5. Template Engine: Reusable notification templates 6. Analytics: Delivery rates, open rates, click-through rates
Key challenges: deduplication, rate limiting per user, retry with backoff, template versioning.
// Notification service flow
// 1. Event occurs -> 2. Build notification -> 3. Route to channels
class NotificationService {
async send(event) {
// 1. Get user preferences
const prefs = await userPrefsStore.get(event.userId);
// 2. Check if user wants this type
if (!prefs.enabled[event.type]) return;
// 3. Check quiet hours
if (this.isQuietHours(prefs)) {
await delayQueue.schedule(event, prefs.quietEnd);
return;
}
// 4. Render template
const content = await templateEngine.render(
event.template,
event.data
);
// 5. Route to enabled channels
const channels = prefs.channels[event.type] || ['in_app'];
for (const channel of channels) {
await messageQueue.publish(`notifications.${channel}`, {
userId: event.userId,
content,
priority: event.priority || 'normal',
dedupeKey: `${event.type}:${event.userId}:${event.entityId}`
});
}
}
}
// Priority levels
// CRITICAL: OTP, security alerts (immediate, all channels)
// HIGH: order updates, payment confirmations
// NORMAL: social interactions, reminders
// LOW: marketing, recommendations (batch-friendly)
The notification service decouples event handling from delivery. User preferences control which channels receive which notification types. Priority levels ensure critical notifications are never delayed.
// Channel adapter interface
class PushAdapter {
async send(notification) {
try {
if (notification.platform === 'ios') {
await apns.send(notification.deviceToken, {
alert: { title: notification.title, body: notification.body },
badge: notification.unreadCount
});
} else {
await fcm.send({
token: notification.deviceToken,
notification: { title: notification.title, body: notification.body }
});
}
return { success: true };
} catch (error) {
if (error.code === 'INVALID_TOKEN') {
await deviceStore.removeToken(notification.deviceToken);
return { success: false, retry: false };
}
return { success: false, retry: true };
}
}
}
// Retry with exponential backoff
class RetryHandler {
async processWithRetry(notification, adapter, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const result = await adapter.send(notification);
if (result.success) return result;
if (!result.retry) return result;
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await sleep(delay);
}
// Move to dead letter queue after all retries fail
await deadLetterQueue.push(notification);
}
}
Each channel has its own adapter with specific error handling. Invalid device tokens are cleaned up immediately. Exponential backoff prevents overwhelming external services during outages.
-- In-app notification storage
CREATE TABLE notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
type VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
body TEXT,
data JSONB,
read BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_notif_user_unread
ON notifications(user_id, created_at DESC)
WHERE read = false;
-- API endpoints
// GET /api/notifications?unread=true&limit=20
// POST /api/notifications/:id/read
// POST /api/notifications/read-all
// Real-time delivery via WebSocket
// When notification is created, also push
// to the user's WebSocket connection if online
// Batch digest for low-priority notifications
// Instead of: "Alice liked your post" x 50
// Send: "Alice and 49 others liked your post"
Partial index on unread notifications makes fetching the unread count very fast. Low-priority notifications are batched into digest summaries to avoid notification fatigue.
Push Notifications, Event-Driven, Message Queue, Priority Queue, Fan-out