Design a Notification System
Adapted from: Alex Xu, System Design Interview – An Insider's Guide (2020), Chapter 10 — rewritten and condensed for teaching.
Notifications deliver breaking news, product updates, events, offers, and payment confirmations, and almost every app has them. A notification is more than a phone buzz. This lesson covers three formats:
- Mobile push notifications (iOS and Android)
- SMS messages
The design leans heavily on two earlier ideas: message queues to decouple the system, and rate limiting to protect users from being overwhelmed.
What you'll be able to do after this lesson:
- Explain how APNs, FCM, SMS gateways, and email services fit into the delivery path.
- Evolve a single-server design into a queue-based, horizontally scalable one.
- Reason about reliability: no data loss, retries, and why exactly-once delivery isn't possible.
- Implement templates, a dispatcher that checks settings, deduplicates, and rate-limits, and retrying workers.
Step 1: Understand the Problem and Establish Scope
| Candidate asks | Interviewer answers |
|---|---|
| Which notification types? | Push, SMS, and email |
| Real-time? | Soft real-time: deliver as soon as possible, but a slight delay under heavy load is acceptable |
| Supported devices? | iOS, Android, and laptop/desktop |
| What triggers notifications? | Client applications, or server-side schedules |
| Can users opt out? | Yes. Opted-out users receive nothing |
| Daily volume? | 10 M push, 1 M SMS, 5 M email |
A quick estimate (assuming an even spread over the day):
| Channel | Per day | Average per second |
|---|---|---|
| Push | 10 M | ≈ 116 |
| SMS | 1 M | ≈ 12 |
| 5 M | ≈ 58 | |
| Total | 16 M | ≈ 185 |
Notification traffic is bursty: a marketing campaign or breaking news can send millions at once. That burstiness is why queues as buffers will matter.
Step 2: High-Level Design
How each notification type works
iOS push
flowchart LR
P[Provider] -- "device token + payload" --> APNS[APNs] --> IOS[iOS device]
- Provider: our server. It builds the notification request and sends it to APNs (Apple Push Notification service). It needs:
- a device token, the unique identifier of the target app install, and
- a payload, a JSON dictionary such as:
{
"aps": {
"alert": {
"title": "Game Request",
"body": "Bob wants to play chess",
"action-loc-key": "PLAY"
},
"badge": 5
}
}
- APNs: Apple's remote service that delivers push notifications to iOS devices.
- iOS device: the client that receives the notification.
Android push
The flow is the same, except Firebase Cloud Messaging (FCM) replaces APNs.
flowchart LR
P[Provider] --> FCM[FCM] --> AND[Android device]
SMS
Use a commercial SMS gateway such as Twilio or Nexmo (now Vonage).
Email
Companies can run their own mail servers, but most use commercial services such as SendGrid or Mailchimp, which offer better deliverability and analytics.
flowchart LR
P[Provider] --> APNS[APNs] --> IOS[iOS]
P --> FCM[FCM] --> AND[Android]
P --> SMSS["SMS service<br/>(Twilio, …)"] --> SMS[SMS]
P --> ES["Email service<br/>(SendGrid, …)"] --> EM[Email]
Contact info gathering
To notify anyone, we need their device tokens, phone numbers, or email addresses. When a user installs the app or signs up, the API servers collect this information and store it:
flowchart LR
U["User installs app / signs up"] --> LB[Load balancer] --> API[API servers] --> DB[(Database)]
user | device | ||
|---|---|---|---|
user_id | bigint | id | bigint |
email | varchar | device_token | varchar |
country_code | integer | user_id | bigint |
phone_number | integer | last_logged_in_at | timestamp |
created_at | timestamp |
Email and phone number live on the user table. Device tokens live on a separate device table, because one user can have many devices, and a push notification can go to all of them.
First design: a single notification server
flowchart LR
S1[Service 1] & S2[Service 2] & SN[Service N] --> NS["Notification server<br/>(+ DB, cache)"]
NS --> APNS[APNs] --> IOS[iOS]
NS --> FCM[FCM] --> AND[Android]
NS --> SMSS[SMS service] --> SMS[SMS]
NS --> ES[Email service] --> EM[Email]
- Services 1…N: anything that triggers a notification, such as a microservice, a cron job, or another distributed system. For example, a billing service emails payment reminders, and a shopping site texts "your package arrives tomorrow."
- Notification system: here, a single server. It exposes APIs to the services and builds payloads for the third parties.
- Third-party services: deliver the notifications. Design for extensibility, so providers are easy to plug in or remove. A provider may also be unavailable in some markets: FCM doesn't work in China, where alternatives such as JPush or Pushy are used.
Three problems:
- Single point of failure. One server.
- Hard to scale. Database, cache, and processing all share one box, so none can scale independently.
- Performance bottleneck. Rendering HTML and waiting on slow third-party APIs ties up the server, which overloads at peak times.
Improved design
Fixes:
- Move the database and cache out of the notification server.
- Add more notification servers with automatic horizontal scaling.
- Add message queues to decouple components.
flowchart LR
S1[Service 1] & S2[Service 2] & SN[Service N] --> NS[Notification servers]
NS <--> C[(Cache)]
NS <--> DB[(DB)]
NS --> QI[[iOS PN queue]] --> WI[Workers] --> APNS[APNs] --> IOS[iOS]
NS --> QA[[Android PN queue]] --> WA[Workers] --> FCM[FCM] --> AND[Android]
NS --> QS[[SMS queue]] --> WS[Workers] --> SMSS[SMS service] --> SMS[SMS]
NS --> QE[[Email queue]] --> WE[Workers] --> ES[Email service] --> EM[Email]
Reading left to right:
- Services 1…N call the notification servers' APIs.
- Notification servers:
- expose send APIs, internal or verified clients only, to prevent spam;
- validate basics such as email and phone formats;
- fetch the data needed to render the notification from the cache or database;
- put the notification on the right message queue for parallel processing.
- Cache holds user info, device info, and templates. The database holds users, notifications, and settings.
- Message queues remove dependencies between components and buffer bursts. There is one queue per notification type, so an outage at one provider (say, the SMS gateway) doesn't block email or push.
- Workers pull events from the queues and call the third-party services.
An example send API (the book labels its email example with an sms path; the request shape is what matters):
POST https://api.example.com/v/email/send
{
"to": [{ "user_id": 123456 }],
"from": { "email": "from_address@example.com" },
"subject": "Hello, World!",
"content": [{ "type": "text/plain", "value": "Hello, World!" }]
}
End-to-end flow:
- A service calls the notification servers' API.
- The notification servers fetch metadata (user info, device token, notification settings) from the cache or database.
- The event goes onto the matching queue. For example, an iOS push goes to the iOS PN queue.
- Workers pull events from the queue.
- Workers send them to the third-party service.
- The third party delivers to the user's device.
Check: high-level design
Queues and providers
1 / 3The SMS provider has a two-hour outage. Why do push and email notifications keep flowing in the improved design?
Pick an answer to see if you are right.
Step 3: Design Deep Dive
Reliability
Preventing data loss
A notification system can delay or reorder notifications, but it must never lose them. To guarantee that:
- Persist every notification in a notification log database before attempting delivery.
- Retry failed sends.
flowchart LR
Q[[Queue]] --> W[Workers]
W --> NL[(Notification log)]
W --> TP[Third-party service]
TP -. failure .-> W
Will recipients get exactly one copy?
No. Delivery is exactly-once most of the time, but in a distributed system duplicates can happen. A worker might send successfully and then crash before recording that it did, so the retry sends the notification again. Exactly-once delivery isn't achievable in general. What you can do is reduce duplicates:
- Give every notification event an event ID.
- When an event arrives, check whether its ID has been seen before. If so, discard it. Otherwise send it and record the ID.
This is the same end-to-end idempotency idea covered in Correctness and Integrity in Dataflow Systems.
Additional components
Notification templates
Millions of notifications a day mostly follow a few shapes, so don't build each one from scratch. A template is a preformatted notification with parameters to fill in:
BODY: You dreamed of it. We dared it. [ITEM NAME] is back — only until [DATE].
CTA: Order Now. Or, Save My [ITEM NAME]
Templates give a consistent format, fewer mistakes, and faster authoring.