System Design Case Study
Notification System
Checkout completes and a "shipped" push is expected, while another user requests a login OTP on a new phone, and a third person gets one grouped "friends liked your post" alert instead of twenty buzzes. Each message feels personal. Under the hood, product events become the right channel for the right user, without spamming them or blocking OTP behind a sale email.
The hard part is not calling once. It is routing at scale: preferences, , provider rate limits, dedup, and delivery tracking when volume hits billions per day.
What happens first?
Ingest + dedup
Accept intent with an .
Durable event; return 202 to the producer.
Rules + prefs
Templates, opt-out, .
Channel workers talk to providers.
Try it: notification fan-out across channels
One event fans out to push, email, and SMS workers with per-channel retries and preference checks.
- Event
- Orchestrator
- Preference filter
- Channel workers
- Provider
- Queued
- 0
- Delivered
- 0
- Mode
- push
- Path hop
- 0
Fan-out
async0 queued, 0 delivered.
Idempotency
event-idWorkers dedupe by notification id so retries do not double-send.
Degradation
providerOne channel failing must not stall the orchestrator for others.
Pick a channel, then walk the fan-out path and watch per-channel failures stay isolated.
What actually happens
An order or auth service posts a send intent with an . The gateway validates and dedupes, then writes to and returns 202 with a notification id. The producer stays fast; delivery is someone else's job.
Stream processors enrich the event, render a template, and check preferences. Marketing respects and frequency caps. Critical OTP traffic uses a separate that bypasses those filters. Push workers call or ; social bursts may use a so the phone shows one update.
At HLD scale: billions of candidates per day across push, SMS, email, and in-app: the design goals stay the same: reliable where it matters, observable end to end, and isolated so a campaign cannot starve a login code. Failures land in a after retries instead of looping forever.
Product event
Order shipped, OTP, social like
Ingest + idempotency
Validate, dedup, 202 accepted
Kafka + rules
Prefs, quiet hours, template render
Priority dispatch
Critical lane separate from marketing
Provider + log
APNs/FCM/SMS/email; track delivery
How to choose the shape of the system
1. Is this transactional or marketing? OTP, payment failed, and fraud alerts need reserved capacity. Digests and promos can wait, batch, and obey .
2. Must the producer wait? Almost never. Accept with an , put the intent on , and let channel workers absorb provider quotas.
3. What if the same event retries? Dedup at ingest, use on push, classify permanent token failures, and park poison messages in a .
Simple rule of thumb
Build a mailroom, not a direct FCM call from every service. Ingest fast, route with rules, isolate critical lanes, and log every status. In-app inbox can store history; push is a hint that something new appeared.
In simple terms: never let a marketing blast share fate with a login code.
Related building blocks
Queues, pub-sub, async patterns, and real-time delivery.
High-level design diagram
Producers → ingest → Kafka → rules → per-channel workers → devices.

Ingest API, preference lookup
Kafka pipeline and dispatch
Rendered payload to providers
Templates, prefs, admin
APNs, FCM, SES, Twilio
Design highlights (from the diagram)
Architecture layer by layer
01Clients, gateway, and ingestionProduct services emit intents; the front door validates and dedupes.
Product services emit intents; the front door validates and dedupes.
In plain terms: A mailroom that accepts letters from every department and stamps each with a tracking number.
Mobile apps, web clients, order and payment services, social features, authentication services, admin tools, and inbound webhooks all produce notification requests. The gateway authenticates the producer, authorizes its tenant and message class, applies rate limits, and routes the request to ingestion. Ingestion validates the payload schema and resolves trusted recipient context rather than accepting an arbitrary tenant ID from the request body.
An accepted intent receives a durable notification ID before the producer gets success. Scope its idempotency key to the producer or tenant and logical operation, and store a request hash so reuse with different content is rejected. This prevents a lost acknowledgement from creating a second intent; downstream send attempts still need their own repeat-safe handling.
Every ingest call should carry an so upstream retries return one logical intent. Keep the same notification ID across routing and delivery records; provider-level idempotency is a separate boundary.
02Event pipeline and routingKafka → rules → priority → per-channel dispatch queues.
Kafka → rules → priority → per-channel dispatch queues.
In plain terms: Sortation center: read the label, check if recipient wants mail, pick air vs ground shipping.
Ingestion appends the accepted intent to a durable topic such as event.ingest. That log absorbs short bursts and lets routing workers recover after a restart. Workers attach locale, template version, and other needed context, then evaluate preferences, quiet hours, frequency caps, and audience eligibility. A finite deduplication window is useful for replay, but must cover the supported retry contract.
Routing produces explicit per-recipient channel work, separated by priority when the latency goals differ. For example, a login code can use reserved worker capacity while a campaign drains gradually under provider quotas. Quiet hours may defer a notification rather than discard it, so record deferred and suppressed outcomes separately. A queue cannot absorb sustained excess demand forever: monitor oldest useful work age and apply admission limits.
Use so OTP and security alerts never share a worker pool with bulk campaigns. Events flow through with on ingest and on routing workers.
03Delivery layer (channel workers)Push, SMS, email, and in-app each have throttled workers.
Push, SMS, email, and in-app each have throttled workers.
In plain terms: Specialist couriers: each obeys different carrier rules and speed limits.
Each channel worker translates one routed intent into its provider protocol. Push workers target APNs, FCM, or Web Push endpoints; SMS workers account for gateway quotas, delivery receipts, and cost; email workers render content and process bounce or complaint evidence. Token buckets and bounded concurrency control attempted sends so retries do not consume unlimited provider capacity.
In-app notifications first enter a durable inbox and can then be announced over WebSocket or SSE. A reconnecting client reads the inbox even if the live hint was lost, and read positions drive unread counts. For external channels, provider acceptance only means the provider accepted responsibility under its contract; delivery to a device or opening by a person requires separate evidence where that channel exposes it.
Mobile push uses to merge bursts. Workers respect provider quotas with token buckets and circuit breakers.
04User preferences and settingsOpt-in per channel and type; quiet hours and caps.
Opt-in per channel and type; quiet hours and caps.
In plain terms: The do-not-call list and “only text me about deliveries” checkbox.
Preferences express which notification types a recipient wants through each channel, along with timezone-aware quiet hours and frequency caps. Apply them again close enough to send time that a recently changed opt-out is respected. Cache entries need a version or bounded lifetime, and queued work needs a policy for changes that occur after it was originally accepted.
Classify urgency by product behavior rather than calling every transaction critical. A requested login code may need immediate delivery, while an order digest can wait until morning. Define which classes may bypass quiet hours, which channels are permitted for fallback, and what happens when preferences cannot be read. Repeatedly guessing an opt-in during a cache outage can turn a reliability incident into unwanted bulk messaging.
Cache preferences with a version and bounded lifetime, then invalidate on update. can defer marketing until a timezone-aware delivery window. Bypass is an explicit policy for eligible urgent events, not an automatic privilege for every message labeled transactional.
05Template and content serviceHandlebars-style bodies, locales, rich media, A/B variants.
Handlebars-style bodies, locales, rich media, A/B variants.
In plain terms: Printed form letters with blanks filled in per recipient and language.
The template service combines a versioned tenant-and-channel template with validated substitution fields and the recipient locale. Persist the chosen version and experiment variant with the intent, so a retry produces the same approved content after a template deployment. Validate missing fields and encode output for its destination instead of blindly interpolating untrusted values into email HTML or links.
Large images and other rich assets belong in object storage and can be delivered through a CDN. Their access and lifetime should match the message: a private account document should not become public merely because a template references it. Preview and canary template changes before a campaign, and retain versioned rendering evidence so support can explain exactly what was sent.
06Tracking, status, and analyticsAccepted, attempted, and observed delivery/read states; evidence differs by channel.
Accepted, attempted, and observed delivery/read states; evidence differs by channel.
In plain terms: Package tracking with scan events at each hub.
Record the notification intent and each provider attempt with stable IDs, timestamps, status, and correlation context. Store evidence as append-only events and derive the current view, because callbacks may arrive more than once or out of order. Verify callback authenticity and map provider-specific states carefully: email bounce, SMS receipt, push acceptance, and client open are different observations.
Support can follow a correlation ID from the originating order through routing, provider attempts, and any inbox read event. Analytics projects this history into ClickHouse or a warehouse for delivery funnels and click-through rates without blocking delivery. Some channels cannot prove individual delivery or reading, and privacy features can distort open signals; label unknown outcomes instead of inventing complete tracking.
07Failure handling and resilienceBackoff, DLQ, fallback channel, circuit breakers.
Backoff, DLQ, fallback channel, circuit breakers.
In plain terms: If FedEx is down, try UPS, or hold the package and alert ops.
Retry temporary failures with capped exponential backoff, jitter, and the notification expiry. A two-minute login code should not be delivered after an hour in a retry queue. Permanent invalid-address or device-token errors should update the endpoint record and stop futile attempts. Malformed or repeatedly failing work goes to a dead-letter queue with a reason and controlled inspection or replay.
A provider timeout can mean that the message was accepted but its acknowledgement was lost. Reuse a supported provider idempotency key or reconcile the attempt before resending when possible. Switching immediately to another provider or channel may duplicate delivery, so define that delay-versus-duplicate trade-off per message class. Circuit breakers isolate failing provider regions or accounts while healthy channels continue serving.
Design for by making each state transition safe to repeat and recording the provider attempt before acknowledging work. Ingest deduplication alone cannot resolve a lost provider response. Use for work requiring repair, and retire permanently invalid endpoints instead of retrying them indefinitely.
08Data stores, admin, and observabilityCassandra logs, Redis cache, campaigns, metrics.
Cassandra logs, Redis cache, campaigns, metrics.
In plain terms: Warehouse records, speed cameras, and the control tower.
Choose storage from the access pattern: transactional preference updates, per-user inbox history, append-only attempts, and analytical scans do different work. Redis can accelerate preference lookups, rate limits, and finite deduplication windows, while durable records support recovery beyond cache loss. Partition by tenant or recipient deliberately and enforce tenant access on derived views as well as source records.
Admin tools need scoped campaign and provider configuration permissions, versioned changes, previews, and a way to stop an unwanted campaign. Observe accepted, deferred, suppressed, provider-accepted, and confirmed-delivery rates separately, along with p99 latency and oldest queue age by priority. One healthy global average can hide a stuck critical lane or one tenant whose provider credentials stopped working.
Transactional vs marketing traffic
Keep OTP and security alerts on a reserved lane so campaigns cannot starve them.
Transactional and time-sensitive messages
A requested login code, password reset, or fraud alert has a different useful lifetime from a receipt. Give genuinely urgent classes reserved capacity through a and dedicated workers, then define when they may bypass . A code that arrives after expiry is a failed experience even if the provider reports delivery.
Low latency also requires reserved provider quota and a bounded retry policy. Falling back from push to SMS or email can help only when the recipient is eligible and the message remains useful. If the first send timed out after possible acceptance, a second channel can create a duplicate; make that trade-off explicit for each class.
Marketing and bulk delivery
Promotions, digests, and re-engagement campaigns can usually batch and wait within a campaign window. Respect , opt-outs, audience eligibility, and per-recipient frequency caps at the appropriate sending boundary. A user who opted out while a campaign waited in the queue should not receive it because a stale routing decision was cached hours earlier.
Use separate worker capacity and, when justified, separate provider accounts or quota allocations so a campaign cannot starve urgent messages. Pause or throttle a campaign when bounce rates, complaints, queue age, or cost exceed its limits. Keep deferred, suppressed, and failed outcomes distinct so operators know whether to wait, stop, or repair the work.
Core flows
Ingest returns quickly; delivery and tracking happen async.
End-to-end delivery
The ingest service accepts a stable notification intent and records it durably before returning its ID. A router evaluates channel eligibility, preferences, priority, and template version; channel workers then call providers under their quotas. Provider callbacks update delivery evidence later. Keep accepted, provider-accepted, delivered, and read as distinct states because the system cannot infer the last two from an HTTP success on send.
Read the diagram definition
sequenceDiagram
autonumber
participant S as Order service
participant GW as API gateway
participant IN as Ingest service
participant K as Kafka
participant R as Rule engine
participant Q as Dispatch queue
participant W as Push worker
participant P as FCM / APNs
participant L as Notification log
S->>GW: POST send (idempotency_key)
GW->>IN: validate + dedup
IN->>K: event.ingest
IN-->>S: 202 notification_id
K->>R: enrich + preferences
R->>R: quiet hours, opt-out, template render
R->>Q: notification.routed (push, high)
Q->>W: dispatch job
W->>P: send push
P-->>W: provider message_id
W->>L: status sent
P-->>L: webhook delivered (async)Critical OTP path
Dedicated SMS pool; bypasses quiet hours.
Read the diagram definition
sequenceDiagram
autonumber
participant A as Auth service
participant IN as Ingest
participant R as Rule engine
participant QS as SMS queue (critical)
participant T as Twilio
participant U as User phone
A->>IN: OTP intent (priority=critical)
IN->>R: skip marketing filters
R->>QS: bypass quiet hours
QS->>T: send SMS
T->>U: deliver OTP
Note over QS: Separate pool from bulk email workersIn-app + real-time
Persist inbox; WebSocket when online.
Read the diagram definition
sequenceDiagram
autonumber
participant K as Kafka
participant W as In-app worker
participant DB as Inbox store
participant WS as WebSocket gateway
participant C as Client app
K->>W: notification.routed (in_app)
W->>DB: append inbox row + bump unread
W->>WS: push if user online
WS->>C: new notification frame
C->>DB: mark read on openWhat users experience
Order shipped
You: Your package leaves the warehouse.
System: Order service emits intent → rules pick push + email → template renders tracking link → delivery logged.
Login OTP
You: You request a one-time code on a new device.
System: Critical priority queue bypasses quiet hours and marketing caps; SMS worker sends within seconds.
Friend liked your post
You: You get one grouped push instead of twenty.
System: Social event uses collapse_key; frequency cap may defer if your feed is noisy; in-app inbox stores history.
Requirements
Channels
- Push (APNs/FCM)
- SMS, email
- In-app inbox
- Web Push
User control
- Per-channel opt-in
- Quiet hours / DND
- Frequency caps
- Unsubscribe compliance
Platform
- Priority classes
- Templates + i18n
- Delivery tracking
- At-least-once + dedup
Numbers that drive the design
Before preference filtering
10B ÷ 86,400
Incident or campaign burst
Push, SMS, email, in-app
Case-study upgrade
Notification Throughput Math
A notification system turns product events into user messages across channels. Transactional messages must not be delayed by bulk campaigns.
Assumptions and formulas
- Total notifications: 10B/day. Peak multiplier: 3x. Channels include push, in-app, email, and SMS-like text.
- Each notification checks preferences, renders a template, enters a priority queue, and writes a delivery log.
- Provider limits and retry amplification are part of capacity, not an afterthought.
Concrete math
- 10B/day = 115.7K/sec average; 3x peak is 347K/sec.
- Channel split: push 5B/day = 57.9K/sec; in-app 3.5B/day = 40.5K/sec; email 1B/day = 11.6K/sec.
- Retry amplification: 2% transient failures x 2 retries adds about 4%, so 10B/day becomes 10.4B attempts.
- Delivery log: 10B/day x 500 bytes = 5 TB/day raw; 30 days is 150 TB before replication and indexes.
- Preference cache: 1B users x 200 bytes = 200 GB raw, so cache active users rather than everyone.
Design decisions forced by numbers
- Separate queues by priority and channel so low-value volume cannot block critical alerts.
- Cache preferences and device tokens, but treat invalid-token cleanup as a feedback loop.
- Use storm control for fan-out events that target millions of users.
Scaling to millions
- At 1M users: one queue per channel and a delivery log are enough.
- At 10M users: add provider throttles, quiet-hours deferral, and retry isolation.
- At 100M+ users: add storm control, fan-out batching, regional workers, and per-tenant budgets.
Readable flow
Interview follow-ups to expect
- Which notifications are transactional and which are optional?
- How do retries avoid duplicate messages?
- What happens when a downstream provider throttles or fails?
Operational readiness checklist
After the math, explain how this capacity plan is measured, protected, launched, and evolved.
Observability
- Attach each capacity estimate to a dashboard metric: p99 latency, queue age, cache hit rate, shard skew, error rate, and freshness lag.
- Alert on rate-of-change and saturation, not only hard failures; backlogs and hot keys usually warn before an outage.
Security and abuse
- Budget for auth checks, rate limits, quota isolation, audit logging, privacy filters, and abuse-review side paths.
- Separate trusted internal events from user-controlled input so retries and backfills cannot bypass policy checks.
Degraded modes
- Define the fallback when a dependency is slow: stale cache, partial results, read-only mode, delayed async work, or admission control.
- Protect the invariant first; reduce freshness, personalization, or analytics quality before risking duplicate or corrupt state.
Testing and rollout
- Test average, peak, and hot-object traffic; include retry storms, cold caches, lagging workers, and regional dependency loss.
- Use canaries, shadow reads, dual-write validation, feature flags, and explicit rollback criteria for risky path changes.
Rollout, backfill, and runbooks
- Make migrations and backfills idempotent, resumable, rate-limited, and visible through progress and error metrics.
- Write the runbook before launch: owner, dashboards, paging thresholds, mitigation switches, replay steps, and repair commands.
10x evolution
- For notification throughput math, state which number forces the next move: cache memory, shard count, write amplification, queue lag, egress, or failover reserve.
- Add the next layer only when the metric forces it: sharding, regional reads, async replay, hot-key isolation, or stricter backpressure.
Storm control and rollout plan
Billions per day requires isolation, budgets, and replay-safe delivery.
SLOs and metrics
Critical OTP p99 <5s, transactional p99 <60s, marketing completion by campaign window. Track queue age by priority, provider throttle rate, retry amplification, duplicate suppressions, and preference cache hit rate.
Degraded modes
If marketing queues flood, reserve critical worker pools. If one provider throttles, slow that channel only. If preferences cache is cold, fail closed for marketing and fail carefully for transactional messages.
Testing and replay
Load test retry storms, chaos test provider 5xx and webhook loss, replay DLQ in dry-run mode, canary template changes by tenant, and verify idempotency before any bulk re-send.
10x evolution
Add per-tenant budgets, regional channel workers, collapse windows for social events, provider quota forecasts, retry isolation, and batch digest paths for low-priority notifications.
Hard-part drill: notification storm control
A popular post creates 50M like events in minutes. Collapse by recipient and post, enforce per-user frequency caps, route social notifications to lower-priority queues, protect OTP and security pools, and replay suppressed events only into in-app counters. Collapse applies only when several events can legitimately become one user-visible message, such as “20 people liked your post.” It cannot discard distinct receipts or security events merely because they share a recipient. Keep the original events available for the required inbox or audit history, and make the aggregation window and maximum frequency explicit.
APIs worth naming
Return 202 plus notification_id only after the intent is durably accepted, not merely schema-validated in memory. The producer can retry the same operation key or query its state after a lost response. Keep acceptance separate from provider acceptance and observed delivery, and authorize status/history access to the correct tenant and recipient.
/api/v1/notifications/sendEmit intent: template, user, payload, priority, idempotency_key/api/v1/users/{user_id}/preferencesChannel and type opt-ins, quiet hours/api/v1/users/{user_id}/preferencesUpdate DND, marketing opt-out/api/v1/users/{user_id}/notificationsIn-app history: sent, read, clicked/api/v1/webhooks/providers/{provider}Delivery receipts (DLR, bounce, open)/api/v1/devices/registerStore FCM/APNs token per deviceEvent pipeline
How the event pipeline absorbs spikes
decouples bursty producers from rate-limited channel workers.
Publishers
Services write intents after gateway validation.
Every accepted intent lands on a durable topic before rules run.
event.ingestIngestion API
Raw notification intent after gateway validation.
Partition by tenant_id or user_id hash for per-user ordering when needed.
notification.routedRule engine
Allowed delivery with channel, template version, priority.
Record the rule result and its version. An opt-out may suppress the intent permanently, while quiet hours may schedule it for a later eligible time. Recheck time-sensitive preferences before sending deferred work.
Consumers
Routing, delivery, and analytics scale independently.
Channel workers respect provider quotas; analytics can lag.
event.ingestEnrichment + dedup worker
Attach locale, device tokens, collapse_key.
The same stable intent ID should produce one routed channel task per permitted recipient and channel. A finite deduplication TTL must cover supported replay; retain durable business identities when retries can outlive that window.
notification.routedChannel dispatch workers
Push, email, SMS, in-app writers.
A dedicated consumer group gives each channel its own copy of relevant events. Separate queues and reserved workers enforce priority; Kafka topic names alone do not cause urgent work to run first. Track lane-specific queue age and provider quotas.
notification.routedAnalytics projector
Stream to ClickHouse for dashboards.
Lag is acceptable; this is not on the delivery critical path.
Simple rule of thumb
Require an at ingest, design for delivery, and send poison messages to a . Use a so chat bursts do not flood the lock screen.
Data model (core tables)
notification_templates
Versioned; rollback if a typo ships to millions.
notification_log
Append-only audit; dedup and support lookups.
user_preferences
Cached in Redis; checked before enqueue.
device_tokens
Invalidate on logout; prune invalid tokens from provider errors.
Bottlenecks and failure modes
Marketing blocks OTP
Risk: One queue for all traffic during a sale email blast.
Mitigation: Priority queues + reserved critical worker capacity.
Duplicate sends
Risk: Producer retry + Kafka redelivery.
Mitigation: Idempotency key at ingest; collapse keys on push.
Invalid push token
Risk: Retry loop wastes provider quota.
Mitigation: Classify permanent failure; delete token; stop retry.
Provider outage
Risk: FCM region down; messages pile up.
Mitigation: Circuit breaker, failover provider, DLQ with replay.
Beginner vs advanced answers
Beginner (5 min)
Use one ingestion contract to record what should be communicated, then let separate channel workers deliver email, push, or SMS. Templates define the content, preferences define permitted channels and timing, and a delivery log tracks attempts and outcomes. Reserving capacity for critical notifications prevents a large campaign from occupying the workers and provider quota needed for time-sensitive account access.
Advanced (deep dive)
The deeper problem is deciding when a repeat is safe. Deduplicate the original intent by business operation and recipient, record each provider attempt, and reuse a provider idempotency key when supported. A send timeout may mean accepted-but-unacknowledged, so switching providers immediately can deliver twice. Reconcile uncertain attempts and make the duplicate-versus-delay trade-off explicit for each notification class.
Choosing and explaining Notification system
A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.
Explain the core idea
Ingest is fast and async; producers get 202 after dedup.
Separate priority queues so OTP is never behind marketing.
At-least-once delivery with idempotency keys and collapse keys.
When it belongs
Design push/email/SMS for a large app.
They ask about retries, dedup, or user preferences.
They probe provider rate limits or incident blast handling.
How it fails
Duplicate OTP or order alerts on retry.
Expired device token retried forever.
Campaign spike delays transactional messages.
Trade-offs to weigh
At-least-once vs exactly-once illusion (idempotent log).
Single provider vs abstraction with failover.
Real-time push vs batched digest email.
Common misunderstandings
One FIFO queue for all channels and priorities.
No quiet hours or frequency caps on marketing.
Skipping delivery log and provider webhook correlation.
Deeper walkthrough
Notification system deeper walkthrough
An event router with templates, preferences, provider integrations, retries, and per-tenant observability.
Requirement refinements
Clarify channels, priority classes, quiet hours, unsubscribe rules, and whether ordering per user matters.
Separate transactional (OTP, security) from marketing: latency and compliance differ.
API design details
Send with idempotency_key; return notification_id and accept async delivery.
Webhook endpoint for provider receipts; GET history for in-app inbox.
Data model and storage
Store the template version with each accepted intent so a retry does not render different content after a deployment. Keep immutable attempt records and a derived current status rather than overwriting all evidence. Tenant-scoped template access and validated substitution fields prevent one integration from rendering another tenant’s content or injecting unintended markup.
A device token identifies a provider delivery endpoint, while user preferences describe whether and when a message should be sent. Track installation ownership and invalidate associations on logout or account changes. Process permanent invalid-token errors to stop futile retries, but do not treat a transient provider outage as proof that every device token is obsolete.
Scaling strategy
Partition routing work by a stable recipient or tenant key when ordering matters, and isolate worker pools and quotas by channel and priority. A blocked SMS provider should not stall email or consume the retry budget for security push. Track oldest useful work age per lane; one global queue-depth metric can hide a delayed critical message behind a large healthy campaign.
Redis for prefs cache, rate limits, dedup windows.
Reliability, recovery, and operations
Retry only failures classified as temporary, using provider retry guidance, jitter, and the message’s expiry. A login code that expires in two minutes should not arrive after hours of queueing. Quarantine malformed or permanently rejected messages with a reason, and require controlled replay after the cause is fixed. Preserve uncertain-send status when a timeout cannot establish whether the provider accepted the message.
Monitor delivery rate, queue lag, provider error class, duplicate suppression count.
Deepening reference
Notification System: realistic production notes
Beginner mental model: Notifications are an event ingestion, preference filtering, provider routing, and retry system.
Production scope: Reliable notifications need templates, user preferences, quiet hours, device tokens, provider failover, deduplication, batching, and observability per tenant.
Choose what the first version must guarantee
Start by deciding which notification system capabilities are essential. Describe the main read and write operations before choosing infrastructure. For each operation, identify the stored records it uses and the condition that makes its result correct. A fast response is useful only if it satisfies that condition.
Next, separate work the user must wait for from work that can finish later. Estimate requests per second, storage growth, and the busiest object or customer. Use those estimates to choose one bottleneck to investigate. The calculations below are example workloads with stated assumptions; they are not measured limits of a particular product.
Concrete capacity calculations
Traffic: 100M daily users receiving 20 candidate notifications/day creates 2B candidates/day, about 23K/sec average. A 10x incident spike is 230K/sec.
Filtering: if preferences, quiet hours, and dedup suppress 60 percent, delivery attempts become 800M/day, about 9.3K/sec average.
Log storage: budget 1 KB per notification decision including template, channel, status, and provider response. 2B decisions/day is 2 TB/day raw; 90-day retention is 180 TB raw.
Retry queues: a 1 percent transient failure rate on 800M attempts creates 8M retries/day. Backoff must spread retries to avoid synchronized retry storms.
Priority isolation: if security messages are 1 percent of traffic, reserve dedicated capacity so 230K/sec low-priority spikes cannot starve about 2.3K/sec high-priority messages.
Scaling to millions of users
Partition queues by tenant, priority, channel, and region. Security and transactional traffic should not share the same worker pool as bulk traffic.
Keep idempotency keys per notification intent so retries and duplicate source events do not spam users.
Cache user preferences and device tokens, but invalidate quickly on unsubscribe, logout, or token refresh.
Use channel-specific rate limits, collapse keys, quiet-hours deferral, and batching to protect users during storms.
Track provider response classes and stop retrying permanent failures such as invalid tokens.
Sequence and data-flow diagram
Walk this path after the high-level design so it is clear what is synchronous, what is asynchronous, and where the system can degrade safely.
Read the diagram definition
sequenceDiagram
participant App as Source service
participant I as Ingest API
participant P as Preference filter
participant Q as Priority queues
participant W as Delivery worker
participant L as Notification log
App->>I: Emit notification intent
I->>P: Dedup and evaluate rules
P->>Q: Enqueue allowed delivery
Q->>W: Dispatch by channel
W->>L: Record status and retry planCore flow to walk through
Ingest event: validate, dedup, resolve template, evaluate user preferences.
Route: choose push, email, SMS, in-app, or webhook provider.
Deliver: send with retry policy, record provider response, update notification log.
Production readiness
Notification System: ship, operate, and evolve
Now examine the same request flow during failures and deployments. Tie each operational control to the user behavior it protects.
Observability
Track notification system p95/p99 latency, error rate, saturation, and user-visible freshness separately for read, write, and async paths.
Alert on leading indicators such as queue age, hot-key skew, cache miss storms, reconciliation mismatches, and error-budget burn.
Security and abuse
Enforce authentication, authorization, rate limits, input validation, audit trails, and privacy checks at the service boundary.
Treat abuse controls as a first-class path: throttling, quota isolation, moderation hooks, revocation, and safe handling of sensitive data.
Degraded modes
Name what still works if cache, search, ranking, stream processing, object storage, or a regional dependency is slow.
Prefer stale-but-safe reads, partial results, async retry, admission control, and feature disablement over corrupting core state.
Testing and rollout
Load test average, peak, and hot-object traffic; add failure-injection for retries, timeouts, backlog growth, and dependency outages.
Ship with canaries, shadow reads or dual writes when useful, rollback switches, schema compatibility checks, and replay validation.
Runbooks
Document dashboards, paging thresholds, owner actions, rollback steps, data repair commands, and customer-impact language.
Keep backfills and replays idempotent, resumable, rate-limited, and observable so repair work does not create a second incident.
10x evolution
Say which metric forces the next architecture change: shard count, cache footprint, write amplification, queue lag, p99 latency, or regional failover reserve.
Evolve one bottleneck at a time: partition by access pattern, isolate hot tenants, regionalize reads, add replayable async pipelines, then harden multi-region behavior.
Hard parts worth deepening
Retry only safe failures; do not retry permanent invalid token errors forever.
Dedup repeated events so users are not spammed.
Respect preference, quiet hours, unsubscribe, and legal requirements.
Use priority queues so OTP/security notifications are not blocked by marketing traffic.
Failure modes and recovery
Push token expires; mark invalid and stop sending until refreshed.
Email provider outage; fail over by route if message class allows it.
Notification storm after incident; batch, collapse, or rate limit low-priority messages.
Template bug affects many tenants; version templates and support rollback.
Signals to monitor
Define delivery success separately for each channel and status boundary: accepted by your pipeline, accepted by a provider, and confirmed by a device are different outcomes. Provider latency helps locate slow external calls.
Watch the oldest retry age against message expiry, not only the retry count. An invalid-token spike points to stale device registrations. Duplicate suppression counts show repeated intents; an unexpected increase may indicate a retry loop upstream.
| Choice | Best when | Cost |
|---|---|---|
| At-least-once delivery | Missing a notification is worse than duplicate. | Requires dedup and collapse keys. |
| Exactly-once illusion | User-visible duplicates are unacceptable. | Usually implemented as idempotent logs, not true exactly-once. |
| Provider abstraction | You need failover and multi-tenant routing. | Lowest common denominator features. |
Try this scenario
Design how password reset OTPs and weekly marketing emails share the platform without affecting each other.
Takeaway
Remember the checkout push and the login OTP: accept intents fast with idempotency, route through rules on a durable bus, and keep critical lanes isolated from marketing. Providers are adapters with quotas, retries, and a DLQ, not the architecture. Never let a campaign share fate with a security code.
Primary sources
- Firebase Cloud Messaging docs: mobile push delivery.
- RFC 8030: Web Push Protocol: web push delivery.
- Kafka Docs: Consumers: notification fanout workers.
Practice and further reading
Use these exercises to connect the lesson to a design decision. State your assumptions, trace what happens to one request, and explain the rule that must remain true even during a failure. That rule is the design's correctness invariant.
Focus for this lesson: Route events through preferences, quiet hours, templates, dedup, provider feedback, retry, fallback, fairness, and delivery observability.
Explain the decision
Why is “exactly once notification delivery” unrealistic?
Read the reasoning
External providers and devices can acknowledge ambiguously; target at-least-once attempts with dedup keys and idempotent user experience.
Apply it to a production scenario
A push token starts returning permanent invalid errors. What should the system do?
Read one approach
Disable/remove it after verified provider feedback, stop retries, update token health, and let clients register a replacement.
Work through the numbers
Two billion events/day remain 40% after preference and dedup filters. What average delivery rate?
Read the worked calculation
800 million/day ≈ 9,259 attempts/s average; peak, retries, and provider quotas drive provisioning.
Reason through a failure
What must quiet-hours processing preserve?
Read the recovery reasoning
Deferred marketing respects the user’s current timezone/preferences and does not delay explicitly exempt critical security events.
Sources and further study
Continue with Design Photo/Video Social Feed.