How to Design a Notification System
AdvancedA notification system delivers messages to users across channels — email, push, SMS, in-app — reliably and at scale. The design decouples the event that triggers a notification from its delivery using message queues, fans out to per-channel workers, respects user preferences and rate limits, and handles the reality that external providers fail. It is a classic system design question because it combines queues, retries, idempotency, and third-party integration.
Think of a mailroom in a large company
When something happens (a package arrives, a decision is made), you do not personally track down each recipient. You drop a note in the mailroom (a queue). Mailroom staff (workers) sort it by delivery method — internal mail, courier, email — respecting each person do-not-disturb preferences, and retry if a courier fails. The event producer never waits; the mailroom absorbs spikes and guarantees eventual delivery. That decoupling is the heart of the design.
Step by Step
Key Concepts
Fan-out
Turning one event into deliveries across multiple channels and recipients. Doing it through queues and separate workers keeps each channel independent and scalable.
Dead-Letter Queue (DLQ)
A queue that captures messages which fail after all retries. It prevents poison messages from blocking the pipeline and lets you inspect and replay failures.
Deduplication / Idempotency
Ensuring a user is not notified multiple times for the same event, even if it is delivered more than once by an at-least-once queue — typically via a notification ID the worker checks.
User Preferences & Rate Limiting
Respecting opt-outs, channel choices, and quiet hours, plus capping how often a user is notified so a flurry of events becomes a digest rather than spam.
Key Facts
- Decoupling with a queue is the key decision — the event producer returns instantly while delivery happens asynchronously and can be retried.
- Message queues deliver at-least-once, so notification workers must be idempotent to avoid double-notifying users on redelivery.
- External providers (APNs, FCM, email, SMS) fail and rate-limit, so retries with backoff and a dead-letter queue are essential, not optional.
Real-World Applications
Order and shipping updates
An order-shipped event fans out to email and push based on the user preferences, retries if the push provider is briefly down, and is deduplicated so a retried event does not send two emails.
Digesting high-volume activity
For a busy social feed, per-user rate limiting batches many events (likes, comments) into a single periodic digest instead of a stream of individual notifications, respecting quiet hours.
Frequently Asked Questions
Why use a message queue in a notification system?
A queue decouples the event producer from delivery. The producer publishes an event and returns instantly, while delivery workers process asynchronously. This absorbs traffic spikes, isolates the producer from slow or failing providers, lets you scale each channel independently, and enables retries — all without making the original request wait.
How do you prevent sending duplicate notifications?
Message queues typically deliver at-least-once, so a worker may receive the same message twice. Make delivery idempotent by attaching a unique notification ID and recording which have been sent, so a redelivered message is recognised and skipped. Combined with per-user deduplication, this ensures one notification per event.
How do you handle notification delivery failures?
Workers retry failed deliveries with exponential backoff, since providers fail intermittently. Messages that still fail after the retry limit are moved to a dead-letter queue for inspection or replay, so a single problematic message does not block the pipeline. This keeps the system resilient to flaky third-party services.
How do you support multiple channels like email, push, and SMS?
A dispatcher reads the user preferences and fans the notification out to per-channel worker pools, each integrating with its provider (email service, APNs/FCM, SMS gateway) and scaling independently. Separating channels means a slow or failing provider on one channel does not affect the others.