System design guide

Design a notification system (push, email, SMS)

A notification system sits between every product team that wants to reach users and the third-party providers that actually deliver push, email, and SMS. The interview is about reliability under fan-out: queues, retries, de-duplication, and not spamming people.

1. Clarify the requirements

Ask: Which channels: mobile push, email, SMS, in-app? Is it transactional (password reset, order shipped), marketing (campaigns), or both? Do we need scheduling and time zones? What delivery latency is expected for each type?

Functional requirements

  • Internal services send a notification to a user (or a segment) with a template and data.
  • Deliver via push (APNs/FCM), email, SMS, and an in-app inbox.
  • Respect user preferences, opt-outs, and quiet hours; support scheduled sends.
  • Track status: queued, sent, delivered, opened, failed.

Non-functional requirements

  • Reliable: an accepted notification is never silently lost.
  • Prioritised: a one-time passcode must not wait behind a 50-million-user marketing blast.
  • No spam: no duplicates, and per-user caps on volume.

2. Estimates

QuantityAssumptionResult
Volume100M users, ~10 notifications each per day across channels1B/day ≈ 12,000/s average
BurstsA campaign to 50M users sent within 10 minutes~83,000/s for that campaign alone
Split70% push, 25% email, 5% SMSSMS ≈ 50M/day, the costly channel
Status log~5 status events × 200 bytes per notification~1 TB/day of event data

Bursts, not averages, size this system, and they come from marketing, so separating transactional from bulk traffic is a requirement, not an optimisation.

3. API

POST /v1/notifications
  { "idempotency_key": "order-123-shipped",
    "user_id": 42, "template": "order_shipped", "data": { ... },
    "channels": ["push", "email"], "priority": "high", "send_at": null }
  → 202 { "notification_id": "..." }

4. High-level architecture

Notification system architecture Product services call the notification API. The notification service de-duplicates by idempotency key, checks user preferences and per-user caps, renders templates, and enqueues one message per channel into priority queues. Channel workers for push, email and SMS send through third-party providers with retries; failures go to a dead-letter queue. Delivery status events flow to an analytics store. read due sends failures Productservices Notification service(dedupe, prefs, caps) Prefs + templates Scheduler Push queue Email queue SMS queue Push workers→ APNs / FCM Email workers→ email provider SMS workers→ SMS providers DLQ + status log
Validate and filter once, then one queue and worker pool per channel and priority.

Notification service. Accepts the request, de-duplicates on idempotency_key, loads user preferences and contact info, drops channels the user opted out of, applies per-user frequency caps and quiet hours (deferring to the scheduler if needed), renders the template, and enqueues one message per channel. It returns 202 as soon as the message is durably queued.

Channel workers. One pool per channel consumes its queue, calls the provider, records the result, and retries transient failures with exponential backoff and jitter. Permanent failures (invalid token, hard bounce) update the user's contact record so you stop trying. Messages that exhaust retries land in a dead-letter queue for inspection.

5. Data model

notifications     notification_id, idempotency_key (unique), user_id, template, data,
                  priority, send_at, created_at
deliveries        notification_id, channel, status, provider, provider_msg_id,
                  attempts, last_error, updated_at
preferences       user_id, channel, category (marketing | security | social ...), enabled,
                  quiet_hours, timezone
devices           user_id, device_id, platform, push_token, last_seen
caps              Redis counter: cap:{user_id}:{category}:{day}

6. Reliability and scaling

  • Priority isolation. Separate queues (or topics) for high-priority transactional messages and bulk marketing, with dedicated workers, so a campaign never delays a login code.
  • Provider rate limits. APNs, FCM, email, and SMS providers all throttle. Workers use a token bucket per provider (see design a rate limiter) and back off on throttling responses.
  • Provider failover. For SMS and email, keep a secondary provider and switch on error-rate thresholds via a circuit breaker.
  • Campaign fan-out. Don't enqueue 50M messages from a single request handler. A campaign job pages through the segment in batches and enqueues gradually, which also lets you pause or cancel it.
  • Idempotency inside workers. A worker can crash after the provider call but before acking the queue. Record (notification_id, channel) as sent before acking, and check it on redelivery.

Templates and localisation

Product teams shouldn't hard-code message text. Store versioned templates per channel and locale (a push title has about 50 characters to work with; an email has a subject, preheader, HTML body, and plain-text fallback). The notification service picks the user's locale, falls back to a default language when a translation is missing, and validates that every variable the template needs is present before enqueueing, so a missing field fails fast at the API instead of producing a broken message in a worker.

Monitoring

  • Queue lag per channel and priority: the earliest signal that a provider is slow or workers are under-provisioned. Alert on the age of the oldest message, not just queue depth.
  • Provider error rates and latency, broken down by error class (throttled, invalid recipient, provider outage), feeding the circuit breakers.
  • End-to-end delivery time from API accept to provider accept, as a p50/p99 per priority, with an SLO for transactional messages.
  • Cost per channel: SMS is expensive, so a bug that sends SMS instead of push shows up first on the bill. Alert on unusual spend.

7. Trade-offs to call out

  • At-least-once vs at-most-once: for a security code, a rare duplicate is better than a lost message; for marketing, at-most-once is often preferred.
  • Render early vs late: rendering in the notification service keeps workers simple; rendering in workers lets you localise and personalise per channel with fresher data.
  • Per-user caps vs business urgency: caps should exempt security and transactional categories.
  • Build vs buy: many teams use a hosted messaging platform for the last mile and only build the orchestration layer.

How to present this in 45 minutes

Spend five minutes on requirements and emphasise the two that shape everything: priority isolation and no duplicates. Two to three minutes on estimates, landing on "bursts drive capacity". Draw the diagram above in about ten minutes and walk one transactional and one campaign message through it. Then go deep on whichever the interviewer prefers: reliability (idempotency, retries, the dead-letter queue) or user experience (preferences, caps, quiet hours). Save the last few minutes for trade-offs and monitoring.

8. Common follow-up questions

  1. How do you send in each user's local time? Store timezone with preferences; the scheduler buckets sends by timezone and releases them at the right hour.
  2. How do you track opens and clicks? Tracking pixels and redirect links for email, client callbacks for push; all feed the status event stream.
  3. How do you avoid notifying a user on a device they've logged out of? Remove the push token on logout and prune tokens the provider reports as invalid.
  4. What about an in-app inbox? Write to a per-user inbox table and push a real-time signal over the app's existing socket, the same connection layer as a chat app.
  5. How do you roll back a bad campaign? Campaign jobs are pausable; messages already handed to providers can't be recalled, so add a review step and a small canary send first.

FAQ

Can a notification system guarantee exactly-once delivery?

Not end to end, because the final hop goes through providers like APNs, FCM, email, and SMS gateways that don't offer it. Aim for at-least-once delivery inside your system with idempotency keys to suppress duplicates, and accept rare duplicates at the provider edge.

Why use a separate queue per channel?

Channels fail and throttle independently. If the SMS provider is slow, a shared queue would back up push and email too. Separate queues (or topics) let each channel's workers scale, retry, and fail over on their own.

Where do user preferences get checked?

Before enqueueing to a channel, in the notification service. Checking early avoids paying for work that will be dropped, and keeps opt-out logic in one place for compliance.