A notification system sends timely messages to people: breaking news, a payment receipt, a shipping update. It sends them in three forms — mobile push (the little alert that pops up on your phone), SMS (a text message), and email. At first it sounds easy: "just call an API and send a message." But the hard parts are scale and reliability: how do you take one event and send it across all three channels, how do you handle sudden bursts of traffic, and how do you make sure you never lose a single notification even when an outside service breaks?
The animation on the right follows one event from start to finish. A service calls the notification servers, which split the event into per-channel message queues (one waiting line each for iOS/APNS, Android/FCM, SMS, and Email). Workers take messages from each queue and hand them to an outside provider. Watch the SMS provider fail and then retry — while the other three channels deliver just fine, completely unaffected.
Step 1: Ask questions and estimate the scale
Notification questions are left vague on purpose. Nail down the scope first:
- Channels: push notification, SMS, and email.
- Devices: iPhone, Android phone, and laptop/desktop.
- Real-time? Soft real-time — send it as fast as you can, but a small delay under heavy load is okay.
- Triggers: notifications come from client or server services, and can also be set up ahead of time on the server.
- Opt-out: yes — if a user turns off a channel, they stop getting messages on it.
Back-of-the-envelope estimation
The interviewer tells us how many messages go out each day per channel:
Push notifications : 10,000,000 / day
SMS messages : 1,000,000 / day
Email : 5,000,000 / day
-----------------------------------------
Total : 16,000,000 notifications / day
Average send rate = 16M / 86,400 s ≈ 185 notifications/sec
185 per second is not a lot on average. But the traffic comes in bursts — a marketing blast or an incident alert can spike to many times the normal rate in seconds. And SMS and email are slow calls to outside companies that sometimes fail. Those two facts — bursts plus slow outside services — are the real reason we need message queues and background workers. It is not the raw number of requests per second.
Start with the per-channel numbers (10M push / 1M SMS / 5M email) and right away name the real problem: "the average rate is low, but traffic is bursty and every send is a slow third-party call, so I will use queues to decouple instead of sending right inline." Sizing first, then letting the numbers explain the design, is the strongest signal you can send.
Step 2: How each channel actually sends
Every channel works the same way — your provider builds a message and calls an outside service, which delivers it to the device — but the players differ:
| Channel | Outside service | Key input |
|---|---|---|
| iOS push | APNS (Apple Push Notification Service) | device token + JSON message |
| Android push | FCM (Firebase Cloud Messaging) | device token + message |
| SMS | Twilio, Nexmo (paid services) | phone number |
| Sendgrid, Mailchimp (paid services) | email address |
Device token
A unique ID that APNS or FCM hands out for each app install. It tells the push service exactly which device to send the notification to. You collect these tokens when the user installs the app or signs up, and store them — so one user with several devices gets the push on all of them.
Where contact info comes from: when a user installs the app or signs up, the API servers collect their contact info and save it. Email and phone go in the user table; device tokens go in a separate device table (one user can have many devices).
A provider can be unavailable in some countries. FCM is blocked in China, so there you fall back to other providers like Jpush or PushY. Build providers to be pluggable from day one — easy to add or swap one without touching the rest of the system.
Step 3: High-level design
The naive version (and why it breaks)
Start with one notification server that takes the API call, builds the message, and calls each provider directly. It has three big problems an interviewer wants you to point out:
- Single point of failure — one server means one thing that can crash and take everything down.
- Hard to scale — the database, cache, and the send logic for every channel are all jammed into one box, so you cannot grow them separately.
- Performance bottleneck — building messages and waiting on slow outside services ties up the server. One slow provider drags everything else down with it.
The improved design
Three changes fix all three problems:
- Move the cache and database out of the notification server.
- Run many notification servers behind a load balancer that adds more servers automatically as load grows.
- Add message queues to decouple the pieces — one queue for each notification type.
Service 1..N ─▶ Notification servers ─┬─▶ iOS PN queue ─▶ iOS workers ─▶ APNS ─▶ iOS
(triggers) (API, auth, validate, │ Android queue ─▶ Android workers─▶ FCM ─▶ Android
template, rate-limit) ├─▶ SMS queue ─▶ SMS workers ─▶ Twilio ─▶ Phone
└─▶ Email queue ─▶ Email workers ─▶ Sendgrid ─▶ Inbox
▲
Cache + DB (user/device/template, notif log)
Reading left to right:
- Notification servers offer internal-only APIs, check that emails and phone numbers are valid, pull the data they need from the cache and database to build the notification, and then drop the event onto the right queue.
- Cache holds user info, device info, and message templates. Database stores user, notification, and settings data.
- Message queues are waiting lines that soak up bursts and keep the pieces from depending on each other. Each notification type gets its own queue, so when one outside service goes down, the other types keep working.
- Workers take events off the queues and call the matching outside service.
A typical send API looks like this:
POST https://api.example.com/v1/sms/send
{
"to": "+14155550000",
"from": "+14155551111",
"template_id": "back_in_stock",
"params": { "item_name": "Sneaker X", "date": "2026-07-01" }
}
Notification template
A ready-made notification you fill in with details, styling, and tracking links instead of writing each message from scratch. Templates keep the look consistent, cut down mistakes, and save time across millions of similar messages.
The phrase that earns the point is "one message queue per channel." It gives you two wins at once: independent scaling (add more workers to whichever queue is backed up) and fault isolation (Twilio going down cannot stall your iOS pushes). Say both benefits out loud.
Step 4: Deep dive — reliability
The single most important rule: a notification system must never lose data. A notification can be late or arrive out of order, but it must never be dropped. Two mechanisms get you there.
Save first, then retry
The notification servers write every event to a notification log database as they put it on the queue, so nothing lives only in memory. When a worker's call to an outside service fails, the event goes back on the queue to try again. If it keeps failing past a set number of tries, an alert pages the developers. This is exactly the SMS-fails-then-retries part in the animation.
Exactly-once? No — remove duplicates instead
| Goal | Reality |
|---|---|
| Exactly-once delivery | Not possible in a distributed system |
| At-least-once + dedupe | What you actually build |
Because retries and the spread-out nature of the system can create duplicates, you add a dedupe check on the event ID (a check that spots repeats). When an event arrives, you check whether you have seen that ID before — if yes, throw it away; if no, send it and record the ID. This cuts down duplicates without pretending exactly-once is achievable.
Do not claim exactly-once — it is a trap. Say "at-least-once delivery using save-and-retry, plus an event-ID dedupe to remove the duplicates that retries always create." Naming that trade-off without being asked reads as senior.
Step 5: Deep dive — the supporting parts
A real notification system is more than just the send path:
| Component | What it does |
|---|---|
| Notification settings | One row per (user_id, channel, opt_in); check opt-in before sending. |
| Rate limiting | Cap how many notifications a user gets, so they do not turn notifications off entirely. |
| Security | appKey / appSecret verify callers — only approved clients can use the push APIs. |
| Watch the queued count | The key health metric: a growing pile of queued notifications means workers are too slow — add more. |
| Event tracking | Open rate, click rate, and engagement go to an analytics service so you understand behavior. |
The settings check is non-negotiable — it enforces the opt-out rule from Step 1, right at send time:
user_id bigint
channel varchar -- push | sms | email
opt_in boolean -- must be true to send this channel
Watch the queue depth. A rising number of queued notifications is the earliest sign that delivery is falling behind. Add workers based on that metric before notifications start arriving late — not after users complain.
Wrap-up
The finished system is scalable, decoupled, and reliable thanks to a handful of deliberate choices:
- Reliability — save to a notification log and retry from the queue, so notifications get delayed, never lost.
- Decoupling — one message queue per channel; a broken provider hurts only its own channel.
- Respect user settings — check opt-in before every send.
- Rate limiting — cap how often you message someone so they do not tune you out.
- Security —
appKey/appSecretso only approved clients can send. - Tracking and monitoring — track events and watch queue depth at every stage for health and insight.
A notification system in 45 minutes: 5 min requirements plus the per-channel volumes → 5 min how each channel sends → 10 min high-level design (the queue-per-channel fan-out) → 20 min deep-dives (reliability/retry/dedupe, settings, rate limiting, monitoring) → 5 min wrap-up. The key idea the interviewer is listening for is decoupling channels with one queue per type — make sure it lands.
Practice
Answer these to check you understood the notification system.
1. Why does the design use one separate message queue per channel?
2. The average send rate is only about 185 per second. So why bother with queues and background workers at all?
3. What is a device token?
4. A worker's call to a provider fails. What happens so the notification is not lost?
5. Why do we accept at-least-once delivery plus a dedupe check instead of exactly-once?