A post office for your app: other services drop off letters ("order shipped"), the post office checks each person's preferences (SMS, email, app), queues the letters by urgency, and hands them to the right courier, retrying when a courier fails.
Design a system that sends notifications to users over push (iOS/Android), SMS, email and in-app channels. Other services (orders, payments, social, marketing) ask it to notify users; it decides the channel, respects user preferences, and delivers reliably at scale without spamming anyone.
50M/day ~ 600/s average; campaigns burst to ~50K/s for minutes
-> queue-based, horizontally scaled workers; providers have their own rate limits
POST /v1/notifications
{ "user_id": 42, "category": "order_update", "template": "order_shipped",
"params": { "order_id": "A123" }, "channels": ["push","email"],
"priority": "high", "send_at": null }
Idempotency-Key: order-A123-shipped
-> 202 { "notification_id": "n_789" }
POST /v1/campaigns { segment_id, template, channels, send_at } -> 202
GET /v1/notifications/{id} -> { status per channel }
PUT /v1/users/{id}/preferences { ... }
services --> Notification API --> validate, dedupe (idempotency key), persist
|
v
[ priority queues ] high (OTP/transactional) low (marketing)
|
Router / Preference workers
- load user prefs, quiet hours, contact info
- render template per channel & locale
- per-channel rate limits (don't spam a user)
|
+-------------+-------------+-------------+
v v v v
[push queue] [sms queue] [email queue] [in-app queue]
| | | |
push workers sms workers email workers in-app store + WebSocket
| | |
APNs/FCM Twilio/... SES/SendGrid (with provider failover)
\_____________ delivery status / receipts ____________/
-> status DB + analytics
notifications(id PK, user_id, category, template_id, params JSON, priority,
status, idempotency_key UNIQUE, created_at, send_at)
delivery_attempts(id PK, notification_id, channel, provider, status, error, attempt_no, ts)
preferences(user_id, category, channel, enabled, PRIMARY KEY(user_id, category, channel))
devices(user_id, device_id, platform, push_token, last_seen, PRIMARY KEY(user_id, device_id))
msg = queue.receive()
if already_sent(msg.notification_id, channel): ack; return # dedupe
try:
provider.send(render(msg)) # with timeout
record(status="SENT"); ack
except TransientError:
requeue with exponential backoff (attempt_no + 1)
except PermanentError (bad token / unsubscribed):
record(status="FAILED"); deactivate token; ack
after N attempts -> dead-letter queue + alert
send_at; a scheduler polls due rows (or uses delayed queues) and enqueues them.