Notification System — Cheatsheet¶
One-page revision. Reproduce from memory and you can drive the interview.
Problem¶
Deliver notifications across push / SMS / email / in-app, triggered by events, at scale, honoring preferences, no spam/dupes.
Profile (say first)¶
Async fan-out + producer/consumer + flaky-downstream resilience. Moderate steady QPS, huge bursty fan-out.
Requirements¶
- Must: accept requests, multi-channel delivery, honor preferences/opt-out, reliable (retry/DLQ/dedup), provider integration.
- Defer: delivery tracking detail, templates/i18n detail, digests, scheduling (→ job scheduler).
- Dominant NFRs: reliable multi-channel · fan-out scale · priority/preference correctness · provider resilience.
Numbers (example)¶
| Sends | 50M DAU × 10/day = 500M/day → ~5k/s avg, ~50k peak |
| Channel mix | push 70% / email 20% / SMS 10% (SMS = expensive, throttled) |
| Fan-out | 1 broadcast → up to 50M sends (the scaling problem) |
| Status/history | ~250 GB/day → TTL + append store (size driver) |
| Prefs/tokens | small, hot → cache |
Architecture¶
services → [Ingestion API] validate·dedup(event_id)·persist → 202
│ enqueue
[Intake Queue]
│
[Fan-out workers] expand per-user · prefs/quiet-hours/opt-out · render template
│ route by CHANNEL + PRIORITY
┌───────┼───────┬───────┐
push q email q sms q in-app (×2: transactional | bulk lanes)
│ │ │ │
workers (rate-limited to provider quota · retry/backoff · circuit breaker)
│
Provider adapters: APNs/FCM · SES/SendGrid · Twilio/SNS
│ webhooks
[Delivery tracking] → status store · prune dead tokens · suppress bounces
Key ideas¶
- Async (202) — never block callers on flaky providers; queues buffer bursts.
- Separate lanes per channel + priority — OTP never waits behind a 50M campaign.
- Channel abstraction — uniform
send()+ per-provider adapters → failover (SES→SendGrid) + isolate quirks. - Preferences/quiet-hours/opt-out at fan-out — precedence: security > category > quiet hours > channel. Opt-out immediate + auditable (CAN-SPAM/TCPA).
- Dedup: at-least-once +
hash(user,event,channel)key + idempotency-key on ingestion → no double-send. - Stream fan-out in batches — never load 50M users into memory.
- Pace campaigns to provider rate limits; per-user caps for fatigue.
Retries / providers¶
Classify errors: 429 → pace + requeue · 5xx → retry backoff+jitter · 4xx → drop + prune. → DLQ + alert. Circuit breaker → failover to secondary provider. Bounce/complaint → auto-suppress address.
Bottlenecks (ranked)¶
- Fan-out amplification → stream batches + buffer + autoscale
- Provider rate limits → per-channel limiter + pacing + multi-provider
- Priority inversion → separate transactional/bulk lanes
- Prefs/device reads → cache
- Status writes → append store + TTL
Failure one-liners¶
- Provider down → circuit breaker → failover / buffer (delayed, not lost).
- 429 → slow lane + requeue (not a failure). Bulk starves OTP → separate lanes.
- Dup → dedup key + idempotency. Opt-out send → enforce at fan-out (legal).
- Poison → DLQ. Dead token → prune. Hard bounce → suppress.
- Ingestion never fails on provider outage (202 + buffer).
Patterns used¶
queue+workers · idempotency · rate limiting · circuit breaker/bulkhead · caching · sharding · job scheduler (scheduled/digests).
SQL vs NoSQL¶
Prefs/devices/templates → KV/relational + cache. Status/feed → wide-column sharded by user + TTL.
Summary line¶
"Async fan-out pipeline: ingestion dedups + 202; fan-out expands per-user applying prefs/quiet-hours/opt-out; separate per-channel + per-priority queues so OTP beats bulk; channel workers rate-limited to provider quotas with retries/DLQ + circuit-breaker failover across a uniform provider abstraction. At-least-once + dedup key = no double-send. Stream broadcasts in batches, pace to provider limits, cache prefs/tokens, TTL the status store."
← Back to README