Requirements & scope
Problem Statement & Requirements
Functional Requirements
- Support multiple notification channels: push (mobile), SMS, email, in-app (web/mobile badge + feed)
- Producers (any internal service) can trigger notifications via an API
- Template-based content with variable substitution (e.g., "{{user}} liked your photo")
- User notification preferences — per-channel opt-in/out, quiet hours, frequency caps
- Notification feed — paginated list of past notifications (in-app inbox)
- Delivery tracking — sent, delivered, opened/clicked, failed
- Priority levels — critical (2FA codes, payment alerts) vs. marketing (promos)
- Rate limiting — prevent notification fatigue (max N notifications per user per hour)
- Scheduling — send at a future time, timezone-aware
- Batching / digest — group related notifications ("3 people liked your photo" instead of 3 separate)
Non-Functional Requirements
- High throughput — 1B+ notifications/day
- Low latency for critical — 2FA codes delivered within 5 seconds
- At-least-once delivery — no notification silently dropped
- Exactly-once display — dedup on the client side
- Scalability — handle viral events (celebrity post → millions of notifications in seconds)
- Observability — per-channel delivery rate, failure rate, open rate
Out of Scope
- Content generation (the calling service provides the content)
- User-facing notification creation (this is an infrastructure service)
- Rich media rendering (handled by client apps)
Scale estimations
Scale Estimations
Traffic
| Metric | Value |
|---|---|
| Total notifications / day | 1B |
| Notifications / second (avg) | ~11,600 |
| Peak notifications / second | ~100K (viral event, breaking news) |
| Push notifications / day | 500M (50%) |
| Email notifications / day | 300M (30%) |
| SMS notifications / day | 50M (5%) |
| In-app notifications / day | 150M (15%) |
Fanout Scenarios
| Scenario | Recipients | Latency Target |
|---|---|---|
| 1:1 (payment receipt) | 1 | < 5s |
| Small group (team mention) | 10-50 | < 10s |
| Large group (channel update) | 1K-10K | < 30s |
| Broadcast (app-wide alert) | 10M-100M | < 5 min |
| Viral fanout (celebrity post) | 50M+ | < 10 min |
Storage
| Metric | Value |
|---|---|
| Notification record size | ~500 bytes |
| Notification feed storage / day | 1B × 500B = ~500 GB/day |
| Retention (90 days) | ~45 TB |
| User preferences | 500M users × 200 bytes = ~100 GB |
Third-Party Costs
| Channel | Provider | Rate Limit |
|---|---|---|
| iOS Push | APNs | ~Unlimited (but throttled per device token) |
| Android Push | FCM | ~Unlimited (batch up to 500 tokens/request) |
| SES / SendGrid | 50K-100K emails/sec | |
| SMS | Twilio / SNS | 1K-10K SMS/sec (varies by country) |
Layered architecture
High-Level Architecture
High-Level ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Detailed Component Architecture
Detailed Component ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
API & contracts
API Design
Send Notification
POST /api/v1/notifications
Content-Type: application/json
X-Idempotency-Key: "order-123-shipped"
Request:
{
"recipients": {
"user_ids": ["u123", "u456"], // explicit user list
// OR
"segment": "premium_users", // audience segment
// OR
"topic": "breaking_news" // pub/sub topic
},
"template_id": "order_shipped",
"template_data": {
"order_id": "ORD-789",
"item_name": "Wireless Headphones",
"tracking_url": "https://track.example.com/xyz"
},
"channels": ["push", "email"], // override user prefs (optional)
"priority": "high", // critical | high | normal | low
"schedule_at": "2026-04-04T09:00:00Z", // future send (optional)
"ttl": 3600, // expire if not delivered in 1h
"collapse_key": "order_update_789", // replace previous with same key
"metadata": {
"source": "order-service",
"campaign_id": "spring_sale_2026"
}
}
Response (202 Accepted):
{
"notification_id": "n_abc123",
"status": "queued",
"estimated_recipients": 2,
"channels": ["push", "email"]
}Query Notification Feed (In-App)
GET /api/v1/users/{user_id}/notifications?cursor=abc&limit=20
Response:
{
"notifications": [
{
"id": "n_abc123",
"type": "order_shipped",
"title": "Your order has shipped!",
"body": "Wireless Headphones is on the way.",
"image_url": "https://...",
"action_url": "https://track.example.com/xyz",
"read": false,
"created_at": "2026-04-03T14:30:00Z"
},
...
],
"next_cursor": "def",
"unread_count": 7
}User Preferences
PUT /api/v1/users/{user_id}/notification-preferences
{
"channels": {
"push": { "enabled": true },
"email": { "enabled": true, "digest": "daily" },
"sms": { "enabled": false }
},
"quiet_hours": {
"enabled": true,
"start": "22:00",
"end": "08:00",
"timezone": "America/New_York"
},
"categories": {
"marketing": { "enabled": false },
"social": { "enabled": true, "channels": ["push"] },
"transactional": { "enabled": true, "channels": ["push", "email", "sms"] }
}
}Data model
Data Model
Notification Log (Cassandra)
Notification Log (Cassandra)Excalidraw diagram · editable shapes · reveal step by stepExplore
Delivery Status (ClickHouse — Analytics)
Delivery Status (ClickHouse — Analytics)Excalidraw diagram · editable shapes · reveal step by stepExplore
Core design decisions
Fanout Strategies
The Fanout Problem
The Fanout ProblemExcalidraw diagram · editable shapes · reveal step by stepExplore
Strategy 1: Fanout-on-Write (Push Model)
Strategy 1: Fanout-on-Write (Push Model)Excalidraw diagram · editable shapes · reveal step by stepExplore
Strategy 2: Fanout-on-Read (Pull Model)
Strategy 2: Fanout-on-Read (Pull Model)Excalidraw diagram · editable shapes · reveal step by stepExplore
Strategy 3: Hybrid (Recommended)
Strategy 3: Hybrid (Recommended)Excalidraw diagram · editable shapes · reveal step by stepExplore
Request flows
Notification Processing Pipeline
Full Flow Diagram
Full Flow DiagramExcalidraw diagram · editable shapes · reveal step by stepExplore
Priority Routing
Priority RoutingExcalidraw diagram · editable shapes · reveal step by stepExplore
Performance & caching
Notification Deduplication & Idempotency
Notification Deduplication & IdempotencyExcalidraw diagram · editable shapes · reveal step by stepExplore
Advanced design
Push Notification Deep Dive
Push Architecture (iOS + Android + Web)
Push Architecture (iOS + Android + Web)Excalidraw diagram · editable shapes · reveal step by stepExplore
APNs (Apple Push Notification Service) Integration
APNs (Apple Push Notification Service) IntegrationExcalidraw diagram · editable shapes · reveal step by stepExplore
FCM (Firebase Cloud Messaging) Integration
FCM (Firebase Cloud Messaging) IntegrationExcalidraw diagram · editable shapes · reveal step by stepExplore
Notification Batching & Digests
Notification Batching & DigestsExcalidraw diagram · editable shapes · reveal step by stepExplore
Rate Limiting & Quiet Hours
Rate Limiting & Quiet HoursExcalidraw diagram · editable shapes · reveal step by stepExplore
Delivery Tracking & Analytics
Delivery Tracking & AnalyticsExcalidraw diagram · editable shapes · reveal step by stepExplore
Template Engine
Template EngineExcalidraw diagram · editable shapes · reveal step by stepExplore
Scheduled & Timezone-Aware Notifications
Scheduled & Timezone-Aware NotificationsExcalidraw diagram · editable shapes · reveal step by stepExplore
Edge cases
Workshop note · added for the website’s common reading format
Walk through three failure moments in Notification System: a request times out before its result is known; a dependency becomes slow rather than unavailable; and a process restarts after committing state but before acknowledging it.
For each case, name the authoritative record, define a safe retry, cap resource usage, and describe what the caller sees. Retry + DLQ should help detect and contain the problem: Retry transient failures with backoff and jitter; quarantine persistent failures for controlled replay.
Tradeoffs
Tradeoffs & Design Decisions Summary
| Decision | Option A | Option B | Chosen | Why |
|---|---|---|---|---|
| Fanout | Fanout-on-write | Fanout-on-read | Hybrid | Write for small audiences + push; read/background for VIP/broadcast |
| Queue | Redis Pub/Sub | Kafka | Kafka | Durability, replay, consumer groups, ordering, DLQ support |
| Priority | Single queue + priority field | Separate topics per priority | Separate topics | No head-of-line blocking; critical path fully isolated |
| Notification DB | MySQL | Cassandra | Cassandra | Write-heavy (1B/day), partition by user, TTL for auto-cleanup |
| Analytics DB | PostgreSQL | ClickHouse | ClickHouse | Columnar, fast aggregations over billions of delivery events |
| Template storage | Database only | Redis cache + DB | Redis + MySQL | Templates are read-heavy, small, and rarely change |
| Scheduling | DB polling | Redis sorted set | Redis sorted set | O(log N) operations, no polling waste, low latency |
| Retry | Immediate retry loop | Exponential backoff + DLQ | Backoff + DLQ | Prevents hammering failed providers; DLQ for investigation |
| Rate limiting | Per-service | Per-user per-channel | Per-user per-channel | Prevents notification fatigue regardless of source |
| Idempotency | None | Redis SETNX with TTL | Redis SETNX | Producers may retry; prevents duplicate notifications |
| Push delivery | Individual sends | Batch API (FCM 500/req) | Batch where possible | 10x fewer API calls to FCM; APNs uses HTTP/2 multiplexing |
| Quiet hours | Drop notifications | Defer to next allowed window | Defer | Don't lose notifications; deliver at appropriate time |
Reliability & fault tolerance
Retry & Dead Letter Queue
Retry & Dead Letter QueueExcalidraw diagram · editable shapes · reveal step by stepExplore
Production architecture
Production Architecture
Production ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Further exploration
Workshop note · added for the website’s common reading format
Rebuild Notification System from memory, then change one assumption: ten times more traffic, a new region, or a stricter consistency requirement. Which component must change first—and which does not?
Compare Policy engine with the same boundary in a related design. Write down one alternative you rejected, what it would simplify, and when you would choose it instead. Follow the source link at the end of this article to explore the original document.
Interview playbook
Interview Tips
Start by clarifying channels — "Which channels do we need? Push, email, SMS, in-app? All of them?" This scopes the design.
Draw the pipeline first — Producer → API → Kafka → Workers → Providers. This is the backbone.
Separate critical from non-critical immediately — "2FA codes and marketing promos must not share the same queue." This shows operational maturity.
Discuss fanout for broadcast — "What happens when a celebrity posts and 50M users need a notification?" Show chunked fanout, FCM topics, and background workers.
User preferences are a first-class concern — Don't treat them as an afterthought. "Users must be able to opt out per channel, set quiet hours, and control frequency."
Rate limiting prevents fatigue — "Without rate limiting, a burst of social activity could send 50 push notifications in a minute." Show per-user per-channel limits.
Idempotency is critical — "The producer might retry. Kafka gives at-least-once. We need dedup at every layer." Show the idempotency key pattern.
Don't forget device token management — "Tokens go stale when users uninstall the app. APNs returns 410 → we must clean up."
Batching shows design depth — "3 people liked your photo" instead of 3 separate notifications. Explain time-window and count-threshold strategies.
End with observability — "How do we know it's working? Delivery rate dashboards, p99 latency, DLQ depth alerts." This is what separates a design from a production system.