Notification System#
Problem statement (interviewer prompt)
Design a notification platform that sends 1B+ messages/day across push (iOS/Android/Web), SMS, and email. Support templates, user preferences (channel + quiet hours + frequency cap), at-least-once delivery with idempotency, and delivery/open tracking.
flowchart LR
P[Producer Service]
Q[(Queue)]
W([Notification Workers])
R[Routing<br/>by channel + prefs]
PUSH((APNS / FCM))
SMS[[Twilio / SNS]]
EM((SES / SendGrid))
USER[(User prefs / device tokens)]
P --> Q --> W --> R
USER -.lookup.-> R
R --> PUSH
R --> SMS
R --> EM
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class P,R service;
class Q,USER datastore;
class SMS queue;
class W compute;
class PUSH,EM external;
flowchart TB
subgraph Producers
APP[App events]
BATCH[Marketing batch]
TXN[Transactional<br/>signup, OTP]
SCHED([Scheduler<br/>reminders])
end
subgraph API[Notification API]
GW[Send API<br/>POST /notify]
TMPL[Template / Personalization<br/>Handlebars / MJML]
DEDUP[Idempotency key]
PRIO[Priority tier<br/>txn / promo / digest]
end
subgraph Queue[Async Bus]
K1[[Kafka: txn topic]]
K2[[Kafka: promo topic]]
K3[[Kafka: digest topic]]
DLQ[[Dead-letter topic]]
end
subgraph Router[Routing]
PREF([User preferences<br/>opt-in / quiet hours])
LIM([Rate limit per user<br/>throttle frequency])
AB[A/B router]
ROUTE[Channel selector<br/>push -> sms -> email fallback]
end
subgraph Channels[Channel Workers]
PW([Push worker])
SW([SMS worker])
EW([Email worker])
IW([In-app worker])
WB([Web Push / WebSocket])
end
subgraph Providers
APNS((Apple APNS))
FCM((Google FCM))
HMS((Huawei HMS))
TW[[Twilio / SNS / MSG91]]
SG((SendGrid / SES / Mailgun))
end
subgraph Devices
iOS
AND[Android]
PH([Phone])
EM[[Email Inbox]]
WEB([Browser])
end
subgraph Storage
PRDB[(User & Device DB<br/>tokens, prefs)]
TPDB[(Templates<br/>versioned)]
HIST[(History / Inbox<br/>Cassandra)]
AUD[(Audit log)]
end
subgraph Feedback
REC[Delivery receipts]
BNC[Bounce / unsubscribe]
ENG[Open / click events]
ML[ML send-time optimizer]
end
Producers --> GW --> DEDUP --> TMPL --> PRIO
PRIO --> K1
PRIO --> K2
PRIO --> K3
K1 --> Router
K2 --> Router
K3 --> Router
PREF -.lookup.-> Router
LIM -.lookup.-> Router
Router --> Channels
Channels --> Providers
Providers --> Devices
Providers -.receipts.-> REC
REC --> ENG --> ML --> PRIO
Channels --> HIST
Channels -.failure.-> DLQ
PRDB --- Router
TPDB --- TMPL
Channels --> AUD
BNC -.update prefs.-> PRDB
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class PREF,LIM,WB,PH,WEB client;
class APP,BATCH,TXN,GW,TMPL,DEDUP,PRIO,AB,ROUTE,AND,REC,BNC,ENG,ML service;
class PRDB,TPDB,HIST,AUD datastore;
class K1,K2,K3,DLQ,TW,EM queue;
class SCHED,PW,SW,EW,IW compute;
class APNS,FCM,HMS,SG external;
Delivery guarantees#
- At-least-once with idempotency key (
tenant, event_id). - Receipts close the loop; without them, system can't optimize send-time.
- Quiet hours, frequency capping per user.
Templating#
- Source of truth in template service (versioned, localized).
- Compile templates once; pass user data context.
Scale knobs#
- Separate topics per priority - txn never blocked by marketing batches.
- Per-provider client pools with circuit breakers.
- Backoff & DLQ on provider 4xx/5xx.
Glossary & fundamentals#
Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Pub/Sub & message brokers | topics, consumer groups, delivery semantics | pub-sub-pattern |
HLD |
Idempotency & retries | safe re-execution, backoff + jitter | idempotency-retries |
HLD |
Resilience patterns | timeout, retry, breaker, bulkhead, backpressure | resilience-patterns |
HLD |
Realtime protocols | WS / SSE / polling / gRPC streaming | realtime-protocols |
LLD |
REST API design | verbs, statuses, pagination, errors | rest-api-design |
LLD |
Async models | futures / async-await / coroutines / actors | async-models |
Quick reference#
Functional#
- Multi-channel: push (iOS/Android/Web), SMS, email, in-app.
- Personalized template rendering.
- User preferences, opt-in, quiet hours.
- Throttle frequency (no spam).
- Track delivery, opens, clicks.
Non-functional#
- 100M+ users; 1B+ notifications/day.
- TXN delivery latency < 5 s; promo < 1 hr.
- 99.9% delivery for txn.
Capacity#
- 1B/day → 12k/s avg, 100k/s peak.
- Per notification: 1-2 KB payload + 0.5 KB metadata.
- Storage (history 90d): 1B × 90 × 2KB = 180 TB.
API#
Schema#
users(id, locale, tz)device_tokens(user_id, platform, token, last_seen)prefs(user_id, channel, enabled, quiet_start, quiet_end)templates(id, version, channel, locale, body)history(user_id, ts, channel, status)(Cassandra)
Trade-offs#
- Fan-out at send vs at read: notifications usually fan-out at send (push to device).
- One queue per priority vs single queue with priority field - split is simpler.
- Provider lock-in: use abstraction layer; APNs and FCM behave differently for receipts.
- Order: typically not required; per-user ordering optional (use partition key = user_id).
Refs#
- Pinterest "Notification platform" blog, Uber/Lyft notification posts, AWS Pinpoint architecture, Twilio Notify, Braze, OneSignal docs.
FAQ#
How do you design a notification system?#
Use a producer queue, stateless workers, a routing layer that joins user preferences, and per-channel adapters for APNS, FCM, SMS, and email with retries and tracking.
How do you guarantee at-least-once delivery without duplicates?#
Stamp each notification with an idempotency key derived from event ID and channel, persist a dedupe table with TTL, and short-circuit duplicate sends on retry.
How do quiet hours and frequency caps work?#
The routing layer joins the user's prefs row before send, drops or defers messages outside allowed windows, and updates a per-user counter to enforce daily caps.
Why use a message queue for notifications?#
A queue absorbs bursts, isolates failures per channel, lets you retry independently, and decouples producers from slow third-party APIs like APNS, Twilio, and SES.
How do you track delivery and opens?#
Emit delivery webhooks from APNS, FCM, and SES into a streaming pipeline, join them with the original send event, and update per-message status for dashboards and ML.
Related Topics#
- Pub/Sub Pattern: fan-out delivery to FCM, APNs, and email is a classic pub/sub use case
- Rate Limiter: notification systems must rate-limit per user and per channel to avoid spam
Further reading#
Curated, high-credibility sources for going deeper on this topic.
- ✍️ Blog - Pinterest Engineering - Notification platform architecture
- ✍️ Blog - Slack Engineering - Real-time messaging at scale
- 📑 Docs - AWS - Architecting multi-channel customer messaging with Pinpoint