Design a Notification System
Multi-channel delivery (push, email, SMS) at scale. Challenges: deduplication, retry logic, ordered delivery, user preferences, and reliability.
1. Requirements
- · Push notifications (iOS via APNS, Android via FCM)
- · Email notifications (SendGrid, SES)
- · SMS notifications (Twilio)
- · In-app notifications
- · User preference management (opt-in/out per channel)
- · Scheduled / delayed notifications
- · Priority levels (critical = immediate, marketing = batched)
- · 10M push notifications per day
- · 1M emails per day
- · 100K SMS per day
- · Each delivery confirmed within 30 seconds
- · At-least-once delivery guarantee
- · Deduplication within 24-hour window
2. Architecture
3. Deduplication
Problem: Kafka guarantees at-least-once delivery. If worker crashes after delivery but before ack, the message is redelivered. User gets same notification twice.
Generate a unique notification_id (UUID v4 or hash of event_id + user_id + channel). Before delivering, check Redis: SETNX notification_id 1 EX 86400. If returns 0 → already delivered, skip.
Before sending, query notification_log DB: SELECT 1 WHERE notification_id = ? AND status = 'delivered'. If exists, skip. Slower than Redis but persistent.
At the Kafka consumer level, track last processed offset per partition in ZooKeeper/DB. On restart, replay only from last committed offset.
4. Retry Logic
5. Device Token Management
Problem: Push tokens become invalid when users reinstall app, get new device, or revoke permissions. Sending to invalid tokens wastes resources and can get your sender ID flagged.
| Scenario | Provider Response | Action |
|---|---|---|
| Token invalid (uninstall/reinstall) | FCM: 404 NotRegistered / APNS: 410 | Delete token from DB immediately |
| Token refreshed | FCM: 200 with new token | Update token in DB |
| App in background (iOS) | APNS: delivered silently | No action needed |
| User notification disabled | FCM: 404 / APNS: 403 | Mark user as unsubscribed for push |
| Rate limit hit | FCM: 429 | Backoff + retry. Implement token bucket for FCM calls. |
6. User Preference System
7. Trade-offs & Bottlenecks
| Decision | Option A | Option B | Chosen |
|---|---|---|---|
| Delivery guarantee | At-least-once (Kafka) | Exactly-once (Kafka transactions) | At-least-once + dedup (simpler, sufficient) |
| Priority handling | Single queue | Separate queues per priority | Separate queues: critical queue never starved by marketing |
| Scheduling | Cron job | Delayed queue (Redis ZADD) | Redis sorted set (score = delivery_at timestamp) |
| Rate to FCM | Send all immediately | Batch + throttle per FCM quota | Throttle: FCM limit is 500K msgs/sec per project |
| Template rendering | Server-side | Client-side | Server-side: easier A/B testing, localization |
→ Token bucket per provider. Kafka consumer reads at controlled pace. If near limit, buffer to Redis and drain slowly.
→ Partition by month. Archive and delete after 90 days (GDPR compliance). Use ClickHouse for analytics queries on delivery metrics.
→ Kafka offset only committed AFTER successful delivery. On restart, Kafka replays unprocessed messages. Dedup prevents double-delivery.