Design a multi-channel notification system supporting push, email, SMS with delivery guarantees.
Published April 27, 2025
Producer Services (Order, Payment, Social)
→ Notification API
→ Message Queue (Kafka)
→ [Channel Workers]
├── Push Worker → APNs (iOS) / FCM (Android)
├── Email Worker → SendGrid / SES
└── SMS Worker → Twilio / SNS
-- Notification requests
CREATE TABLE notification_requests (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
type VARCHAR(50), -- 'ORDER_SHIPPED', 'MESSAGE_RECEIVED'
channel VARCHAR(20), -- 'PUSH', 'EMAIL', 'SMS'
payload JSONB,
status VARCHAR(20), -- PENDING, SENT, FAILED
created_at TIMESTAMP,
sent_at TIMESTAMP
);
-- User notification preferences
CREATE TABLE notification_preferences (
user_id UUID,
channel VARCHAR(20),
type VARCHAR(50),
enabled BOOLEAN DEFAULT TRUE,
PRIMARY KEY (user_id, channel, type)
);
// Notification Worker (per channel)
public void processNotification(NotificationEvent event) {
// 1. Check user preferences
if (!preferences.isEnabled(event.userId, event.channel, event.type)) {
return; // user opted out
}
// 2. Deduplication check
String dedupeKey = event.userId + ":" + event.idempotencyKey;
if (redis.setnx(dedupeKey, "1") == 0) {
return; // already sent
}
redis.expire(dedupeKey, 86400); // 24h window
// 3. Send via provider
boolean sent = false;
for (int retry = 0; retry < 3 && !sent; retry++) {
try {
sendViaProvider(event);
sent = true;
} catch (Exception e) {
Thread.sleep(exponentialBackoff(retry));
}
}
// 4. Update status
notificationRepo.updateStatus(event.id, sent ? "SENT" : "FAILED");
// 5. Requeue on failure (dead letter queue)
if (!sent) deadLetterQueue.publish(event);
}
Primary provider fails:
1. Retry with exponential backoff (1s, 2s, 4s)
2. Fallback to secondary provider
3. Dead letter queue for manual retry
Queue backpressure:
- Multiple workers per channel
- Auto-scale workers based on queue depth
High Priority: security alerts, OTP codes → immediate
Medium Priority: order updates → within 1 minute
Low Priority: marketing, newsletters → within 1 hour
Separate Kafka topics per priority