System Design
Design a Notification System
It sounds like 'just call a push API.' Then you account for millions of notifications a second, three delivery channels, third-party providers that fail, and never spamming a user twice - and it becomes a masterclass in queues and reliability. Here's the design.
"Design a notification system" - a service that sends notifications to users across channels like push, email, and SMS - is a question I like precisely because it sounds easy and turns out not to be. Candidates often start with "I'll just call the push notification API," and then I start adding the realities: millions of notifications per second, three different delivery channels, third-party providers that are slow or temporarily down, and an ironclad rule that you never spam a user or send the same notification twice. Suddenly the "easy" question is a masterclass in message queues, reliability, and scale - which is exactly why it is such a good interview, and why so many candidates underestimate it until it's too late.
Here's how to design it, with my notes on where the points are won.
Scope the requirements
The functional core:
- Send notifications across multiple channels - push (mobile), email, and SMS.
- Handle high volume - potentially millions of notifications.
- Respect user preferences - users choose what they receive and on which channels.
- Support different triggers - system events, user actions, scheduled notifications.
The non-functional requirements that shape the design:
- Reliability - notifications must not be lost. If a payment receipt fails to send, that's a real problem.
- Scale - handle huge bursts (a marketing blast to millions) without falling over.
- No spam / no duplicates - never send the same notification twice, and respect rate limits so users aren't overwhelmed.
- Channel independence - a failure in one channel (email provider down) shouldn't block the others.
Interview: The realization I'm waiting for is that this is fundamentally a messaging and reliability problem, not an API-calling problem. The naive "call the notification API directly" answer falls apart the moment you consider volume, provider failures, and bursts. Candidates who recognize that this needs to be built around queues - to decouple, absorb spikes, and retry reliably - have grasped the real shape of the problem. That framing is the foundation of every good answer here.
The core: build it around a queue
The single most important decision in this design is to put a message queue at the center. When something triggers a notification, the triggering service doesn't send it directly - it drops a notification request onto a queue, and a separate set of workers processes the queue and handles the actual sending. This decoupling is the key to everything that follows. (See the message queues guide - this is that pattern doing exactly what it's for.)
Why the queue matters so much here:
- It absorbs bursts. A marketing blast might generate millions of notifications in an instant. Without a queue, that flood would slam directly into the sending logic and the third-party providers, overwhelming them. With a queue, the burst piles into the queue and workers drain it at a sustainable rate - the queue is a shock absorber.
- It enables reliability. If sending a notification fails (a provider times out), the queue allows a retry - the notification isn't lost, it goes back to be tried again. Work isn't dropped on failure.
- It decouples the trigger from the delivery. The service that triggers a notification returns instantly after queuing it; the slow work of actually sending happens asynchronously in the workers.
graph TD
T[Triggers: events, actions, schedules] --> NS[Notification Service]
NS --> PREF{Check preferences<br/>& rate limits}
PREF --> Q[Notification Queue]
Q --> W[Worker fleet]
W --> PUSH[Push sender → APNs/FCM]
W --> EMAIL[Email sender → email provider]
W --> SMS[SMS sender → SMS provider]
This diagram shows a notification system built around a queue. Triggers - events, user actions, schedules - reach the Notification Service, which checks user preferences and rate limits, then places notification requests onto a queue. A worker fleet pulls from the queue and dispatches each notification to the appropriate channel sender: push notifications to mobile push providers, emails to an email provider, and SMS to an SMS provider. The queue decouples triggering from sending, absorbs bursts, and enables retries.
Channel handling: send to providers
The system itself doesn't deliver push notifications, emails, or SMS directly to devices - it hands off to third-party providers for each channel (a mobile push service, an email provider, an SMS gateway). So the workers, after pulling a notification from the queue, route it to the appropriate channel-specific sender, which formats it correctly and calls that channel's provider.
Crucially, the channels should be independent: if the email provider is down, push and SMS should keep working. This argues for separate queues or separate worker pools per channel, so a slow or failing provider in one channel doesn't back up the others. A candidate who notes that channel independence requires isolating the channels - so one provider's failure doesn't block the rest - is thinking about real failure modes.
The third-party providers also introduce a reliability wrinkle: provider calls can fail or be slow, so the senders need retry logic with backoff (retry a failed send, waiting longer between attempts), and a way to handle a notification that repeatedly fails (eventually moving it to a dead-letter queue for inspection rather than retrying forever). This is the same reliability machinery queues provide generally, applied to flaky external providers.
Preferences and the anti-spam problem
A critical part that separates a thoughtful design: respecting user preferences and not spamming people. Before a notification is sent, the system checks the user's preferences - do they want this type of notification, on which channels, and is now an acceptable time? A user who disabled email notifications shouldn't get one.
There's also rate limiting and deduplication: a user shouldn't receive twenty notifications in a minute because of some event storm, and they shouldn't get the same notification twice. So the system applies rate limits per user (capping how many notifications they receive in a window - exactly the rate-limiting building block) and deduplicates (recognizing and dropping duplicate notification requests, often via a unique key per logical notification). This is also where idempotency matters: because queues can deliver a message more than once and providers can be retried, the sending logic must ensure a notification isn't sent twice even if its request is processed twice. (See the rate limiting and idempotency guides.)
Interview: The anti-spam and reliability details are where senior candidates distinguish themselves. Anyone can say "send the notification." The strong answer adds: check preferences before sending, rate-limit per user so they're not overwhelmed, and deduplicate so the same notification never goes twice - and make the whole thing idempotent because queue redelivery means a request might be processed more than once. Raising the "don't send it twice" problem unprompted, and tying it to idempotency, is exactly the kind of thinking that tells me a candidate has built real systems.
Scheduling and templates
Two supporting pieces round out the design. Scheduled notifications (send this at 9 a.m., or remind me tomorrow) need a scheduling component that holds notifications until their time and then enqueues them - which at scale means efficiently tracking potentially millions of future scheduled notifications and enqueueing them as their times arrive, without scanning everything constantly. Templates let notifications be defined once (with placeholders for user-specific data) and rendered per recipient, rather than constructing each message from scratch - important when sending the same notification, personalized, to millions of users. Neither is the crux, but mentioning them shows a complete picture of what a real notification platform must support.
Deep dive: estimating the scale
A quick estimate shows why the queue is non-negotiable. Suppose a large platform sends billions of notifications per day across channels - that averages tens of thousands per second, but the real challenge is bursts: a single marketing campaign or major event can trigger millions of notifications in seconds, dwarfing the average. This burstiness is the defining load characteristic, and it's exactly why a queue sits at the center - it absorbs a spike that would otherwise overwhelm the senders and the third-party providers (which have their own rate limits). Stating that the system must handle not just high average volume but extreme bursts - and that the queue is what makes bursts survivable - shows you understand the real load pattern.
Deep dive: priority and notification types
Not all notifications are equal, and a thoughtful design accounts for it. A one-time password or a critical security alert is far more urgent than a promotional message. Mixing them in one undifferentiated queue means a marketing blast could delay an urgent OTP stuck behind millions of low-priority messages. The solution is priority handling - separate queues or priority levels, so urgent transactional notifications are delivered promptly even during a low-priority flood. A candidate who recognizes that notifications have different urgencies, and that a marketing blast shouldn't delay a security code, is thinking about a real failure mode naive designs miss. This pairs with the channel separation already discussed - transactional and promotional traffic on separate paths so they don't interfere.
Deep dive: tracking delivery and feedback
A production notification system needs to know what happened to each notification - was it delivered, did it fail, did the user open it? Providers report back delivery status (and failures), often via callbacks - which is itself a webhook pattern, the provider calling your system to report "this push was delivered" or "this email bounced." The system records these outcomes, both to retry genuine failures and to feed analytics (delivery rates, open rates) and to stop sending to addresses that consistently bounce. A candidate who notes that delivery tracking and provider feedback (often via webhooks) close the loop - turning fire-and-forget into reliable, observable delivery - shows a complete understanding. (See the webhooks guide for how provider callbacks work.)
Deep dive: scaling the workers and providers
A practical scaling point: the worker fleet that drains the queue must scale with load, and queue depth is the signal that tells you when to scale. During a burst, the queue grows; you add workers to drain it faster; as it empties, you scale back down. This elastic scaling - driven by queue depth - is one of the cleanest benefits of the queue-centric design. But there's a constraint the workers must respect: the third-party providers have their own rate limits and capacities. You can't throw a thousand workers at an email provider that accepts only so many requests per second - you'd be throttled or blocked. So workers must pace their sending per provider, which means the queue may intentionally drain more slowly for a constrained channel while faster channels keep up. A candidate who recognizes that you scale workers by queue depth but must throttle to each provider's limits - balancing your scale against theirs - is showing end-to-end thinking. In a system built on third-party providers, your throughput is ultimately bounded by theirs, and the design must respect that.
What I'd push a senior candidate on
For senior candidates, I probe the harder edges. How do you guarantee a critical notification (a payment receipt) is eventually delivered even if a provider is down for an extended period (durable queuing, retries with backoff, and eventually a dead-letter path for investigation)? How do you prevent a buggy trigger from spamming millions of users (rate limiting, and circuit breakers that halt a runaway notification source)? How do you handle a provider's own rate limits, since you can't send faster than they accept (throttling workers per provider)? How do you make the whole pipeline idempotent end to end so a retry never doubles a notification? These probe whether the candidate can reason about the reliability guarantees and failure containment that a real notification system - which users trust for important messages - actually requires.
Putting it together
The full architecture:
- A notification service receives triggers, checks preferences and rate limits, and enqueues notifications.
- A queue (per channel, for independence) decouples triggering from sending, absorbs bursts, and enables retries.
- A worker fleet pulls from the queues and dispatches to channel-specific senders (push, email, SMS), each calling its third-party provider with retry and backoff.
- Deduplication and idempotency ensure no notification is sent twice.
- Scheduling and templating support timed and personalized notifications.
| Concern | The trap | The strong answer |
|---|---|---|
| High volume / bursts | Call the API directly | Queue absorbs bursts, workers drain steadily |
| Reliability | Lose it on failure | Queue + retries + dead-letter |
| Channel failure isolation | One pipeline for all | Separate queues/workers per channel |
| Spamming users | Send everything | Preferences + per-user rate limiting |
| Duplicates | Send twice | Deduplication + idempotency |
Interview: What gets you hired on this question: recognizing it's a messaging-and-reliability problem and building it around queues (for bursts, decoupling, and retries), isolating channels so one provider's failure doesn't sink the rest, and handling the human side - preferences, rate limiting, no duplicates - with idempotency throughout. The candidates who struggle treat it as "just call the push API" and never confront volume, failure, or spam. See it as a queue-and-reliability design, and the whole thing comes together. The phrase I most want to hear is "I'll put a queue at the center" - because everything good about the design follows from that one decision.
The bottom line
A notification system looks like "call an API" and is actually a messaging-and-reliability design. The core decision is to build it around a message queue, which absorbs bursts (a blast to millions), enables retries so notifications aren't lost when providers fail, and decouples triggering from sending. Channels (push, email, SMS) are handled by separate senders calling third-party providers, isolated so one channel's failure doesn't block the others, with retry and backoff for flaky providers, and priority handling so an urgent security code never waits behind a marketing flood. And the human side - checking preferences, rate-limiting per user, and deduplicating with idempotency so no one is spammed or notified twice - is what separates a thoughtful design from a naive one.
See it as a queue-centric reliability problem, and the "easy" question reveals itself as a strong test of how you handle scale and failure. We work through case studies like this end to end across System Design Fundamentals.
Go deeper
- The System Design Primer - building blocks for queue-based, high-throughput systems.
- Message queue (Wikipedia) - the core component this design is built around.
- Push technology (Wikipedia) - how notifications are pushed to devices.
- Amazon Builders' Library - how reliable, high-volume services are built in practice.