← All designs

Medium · reference design

Notification system

Send push, email and SMS on behalf of every product team: urgent messages in seconds, campaigns at their own pace, user preferences respected and no visible duplicates.

1 Requirements

Functional

  • Product services send a notification to a user through one API, choosing a template and a priority.
  • Deliver through push, email or SMS, according to each user’s preferences and opt-outs.
  • Track the status of every notification: queued, sent, delivered or failed.

Non-functional

  • Urgent messages (sign-in codes, security alerts) arrive within seconds, even during a large campaign.
  • At-least-once delivery with no duplicates the user can see.
  • A failing provider affects only its own channel.
  • Handle bursts of hundreds of thousands of notifications a minute.

Out of scope: Designing the message content and templates editor. In-app inbox UI.

2 Core entities

Notification
Who, what template, which data, priority and an idempotency key.
Preferences
Per user and channel: allowed, quiet hours, opt-outs.
Template
The message per channel and language, with placeholders.
Delivery attempt
One try on one channel: provider, result, timestamps.

3 API

POST /notifications

Body: userId, templateId, data, priority, idempotencyKey. Returns 202 with a notificationId.

GET /notifications/{id}

Status per channel and the delivery attempts so far.

PUT /users/{id}/preferences

Channels allowed, quiet hours and opt-outs.

4 Data flow

  1. A service calls the API; the request is validated and deduplicated.
  2. Preferences decide which channels, if any, the user gets.
  3. The router renders the template and puts the message on a queue for its priority.
  4. Channel workers send through the provider, retrying on failure, and record every attempt.

5 High-level design

Press play to follow one request through the system. Every step explains what happens and the trade-off behind the choice.

Product serviceAsks to notify
Notification APIValidate + dedupe
PreferencesChannels, opt-outs
RouterTemplate + priority
Urgent queueCodes, alerts
Standard queueActivity
Bulk queueCampaigns
Channel workersPush, email, SMS
Delivery logEvery attempt
ProvidersPush, email, SMS

Follow one notification from the service that asks for it to the provider that delivers it. Press play, or step through the stages to see what happens and what each choice costs.

6 Deep dives

How do we avoid duplicates with at-least-once queues?

OptionGivesCosts
Trust the queueNothing to build.Redelivery after a worker crash sends the message again.
Check the delivery log before sendingOne lookup per send catches redeliveries.A crash between the send and the log write can still duplicate.
Log check plus provider idempotency keyCloses the last gap where the provider supports keys.Not every provider supports them.

Choice. Check the log and pass an idempotency key where the provider supports it. The remaining gap is small and only on providers without keys.

How do we not overwhelm providers or users?

OptionGivesCosts
Send as fast as workers canSimple.Hits provider rate limits and can spam users.
Token buckets per provider and per userStays under provider quotas and caps messages per user per day.Bulk sends take longer.

Choice. Token buckets per provider and per user, with urgent messages exempt from the per-user cap.

How does a campaign to millions of users start?

OptionGivesCosts
The caller sends one request per userNo new component.Millions of API calls from one service, hard to pause or cancel.
A fan-out job expands an audience in batchesOne request starts it; it can be paused, resumed and throttled.Another job to run and monitor.

Choice. A fan-out job that writes batches into the bulk queue, so a campaign can be throttled or stopped halfway.

In one sentence

One API that accepts fast and deduplicates, preferences first, a queue per priority, workers per channel with safe retries, and every attempt on record.