Design problems
Messaging & collaborationMediumSDE-1 30 min

Design a notification system (push, SMS, email)

Asked at
AmazonFlipkartPhonePeSwiggyUberZomato

The interviewer asks

“Design a notification platform that other teams in the company call to send push notifications, SMS, email and WhatsApp messages: OTPs, order updates, and marketing campaigns to 100M users. It must respect user preferences and never spam or lose important messages.”

What a strong answer looks like

A strong answer treats this as a shared platform with a small API and a large pipeline. Producers call one endpoint with an idempotency key, a template ID and a user ID. The service validates, deduplicates, resolves preferences and quiet hours, renders templates, and puts messages on priority-separated queues per channel, so a 50M-user marketing blast never delays an OTP. Channel workers call providers (FCM, APNs, SMS gateways, SES) with per-provider rate limits, retries with backoff, and failover between providers. Delivery status flows back via provider webhooks into a status store for tracking and analytics. India specifics, such as DLT template registration for SMS and TRAI rules on promotional hours, show real-world awareness.

Concepts to mention

Async API with idempotency keysPriority-separated queues (bulkheads)Template rendering and localisationUser preferences, quiet hours, frequency capsPer-provider rate limiting with token bucketsRetries with backoff, DLQ and circuit breakersProvider failover and routing by success rateDelivery receipts via webhooksAt-least-once with dedupDLT/TRAI compliance for SMS in India

1 Requirements

Functional

  • Single API for internal services to send a notification to a user on one or more channels
  • Channels: mobile push (FCM/APNs), SMS, email, WhatsApp, in-app inbox
  • Templates with variables and localisation (English, Hindi and other languages)
  • User preferences: opt out per category and channel, quiet hours, frequency caps
  • Scheduled and bulk campaigns to segments of millions of users
  • Delivery tracking: sent, delivered, opened, failed, with retries and fallbacks (push fails → SMS)

Non-functional

  • Transactional (OTP, payment) latency: p99 under 5 s end to end to the provider; OTP ideally under 2 s
  • Throughput: 100M notifications/day, campaign bursts of 50M within 30 minutes
  • No loss of transactional messages; at-least-once delivery with dedup
  • Marketing traffic must never slow transactional traffic
  • 99.99% availability of the send API
  • Compliance: DLT-registered SMS templates in India, unsubscribe in email, no promotional SMS at night

Out of scope (say it out loud)

  • Building our own SMS gateway or email MTA
  • Campaign segment builder UI
  • Rich in-app messaging UI design

2 Estimates

Daily volume~100M/day (~1,200/s average)Mix: ~60M push, ~25M email, ~10M SMS, ~5M WhatsApp
Campaign burst~30k/s50M pushes in 30 min ÷ 1,800 s ≈ 28k/s
Transactional peak~5k/sOrder and payment updates during dinner peak or a sale; OTPs during login spikes
SMS cost~₹10-25 lakh/day10M SMS × ~₹0.10-0.25 per SMS; this is why push-first with SMS fallback matters
Status events~300M/day~3 status updates per notification (sent, delivered, opened)
Status storage~30 GB/day, keep 90 days hot300M × ~100 bytes

3 API

EndpointWhat it does
POST/v1/notificationsSend {idempotency_key, user_id, template_id, data, channels?, priority, category}; returns notification_id
POST/v1/campaignsSchedule a bulk send to a segment ID with template and send window
GET/v1/notifications/{id}Status per channel: queued, sent, delivered, failed
PUT/v1/users/{user_id}/preferencesUser opt-in/out per category and channel, quiet hours, language
POST/v1/devicesRegister or refresh a push token for a user device
POST/v1/webhooks/{provider}Delivery receipts and bounces from providers

4 Data model

notificationsCassandra / DynamoDB
notification_id (PK)idempotency_key (unique index)user_idtemplate_idcategoryprioritycreated_atper-channel status

Write-heavy and looked up by ID or user; TTL 90 days. Idempotency key table enforces dedup with a conditional insert.

templatesPostgres
template_idchannellocalebodyvariablesdlt_template_idversioncategory

Small, versioned, cached in memory by workers.

preferencesPostgres + Redis cache
user_idcategorychannelenabledquiet_hourstimezonelanguage

Read on every send, so cached; changes invalidate the cache.

devicesDynamoDB / Cassandra
user_iddevice_idplatformpush_tokenapp_versionlast_seen

Tokens removed when FCM/APNs reports them invalid.

rate countersRedis
user_id:category:day → count; provider:second → count

Frequency caps per user and throughput caps per provider.

inboxCassandra
user_idcreated_atnotification_idtitlebodyread

In-app notification centre, partitioned by user.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALsend()idempotent insertbatched usersprefs, capsOTP, orderspromosdelivery receiptsupdate statusInternal servicesNotification APIStatus / webhookserviceRouter (prefs, render)Campaign schedulerQueues: transactionalQueues: marketingChannel workersNotifications DBRedis (prefs, caps)SMS / WhatsAppprovidersFCM / APNsEmail (SES / SendGrid)
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Order-delivered notification

  1. 1Order service calls POST /notifications with idempotency_key = order_id + event, template order_delivered, priority high
  2. 2API does a conditional insert on the idempotency key; a duplicate call returns the existing notification_id
  3. 3API writes the request to the transactional Kafka topic and returns 202 within ~20 ms
  4. 4Router loads preferences (cache), checks the category is allowed, picks channels (push + in-app), resolves language, renders the template
  5. 5Router places one job per channel on the high-priority channel queue
  6. 6Push worker fetches the user's device tokens, calls FCM/APNs in batches, records sent status; invalid tokens are deleted
  7. 7If no device is reachable or push is not acknowledged and the category requires it, a fallback rule schedules SMS after N minutes

OTP over SMS

  1. 1Auth service sends with priority critical and channel sms; no frequency cap except anti-abuse limits per phone number
  2. 2Router uses the DLT-registered OTP template and skips quiet hours (transactional)
  3. 3SMS worker sends via the primary provider; if it errors or times out within ~2 s, it retries on a secondary provider
  4. 4Provider delivery receipt arrives by webhook and updates status; success rates per provider and operator feed into routing

Diwali campaign to 50M users

  1. 1Marketing schedules a campaign for a segment and a send window (e.g. 7-9 pm)
  2. 2Campaign scheduler streams user IDs from the segment store in pages of 10k and enqueues them on the marketing topic at a controlled rate
  3. 3Router filters opted-out users, users over the daily promo cap, and users in quiet hours (defer them)
  4. 4Marketing workers send at provider-allowed rates; transactional queues keep their own reserved worker pool, so OTPs are unaffected

7 Deep dives

Priority isolation: never let a campaign delay an OTP

The most common failure of notification systems is head-of-line blocking: a 50M-message campaign fills the queue and a login OTP waits behind it for 20 minutes. Fix it structurally, not with priority fields inside one queue. Use separate topics per priority and channel (critical-sms, transactional-push, marketing-push, marketing-email) with separate consumer groups and reserved worker capacity for the critical ones. Rate limits to providers are also split: reserve, for example, 30% of the SMS provider's throughput for OTPs, and let marketing use the rest only when free. Apply admission control at the API: campaign traffic is throttled at the scheduler, so it enters the pipeline at a steady rate instead of 50M at once. Monitor queue lag per topic with SLOs (critical lag under 1 s, marketing lag under 30 min). This is the same bulkhead pattern used in other systems, and naming it explicitly shows you have seen real incidents. Also separate per tenant: one internal team's bug that sends 10M messages should hit its own quota, not everyone's.

Exactly-once feeling: idempotency and dedup

Users hate duplicates ('Your order is delivered' three times) and losing an OTP is worse. Between our service and providers, true exactly-once is impossible: if a call to the SMS provider times out, we do not know whether it was sent. So the design is at-least-once with dedup at each step. Producers send an idempotency key; the API's conditional insert makes repeated calls safe. Kafka consumers may redeliver after a crash, so each channel job has a deterministic job ID (notification_id + channel) and workers check a sent-status record before calling the provider, updating it after success. For the ambiguous timeout, we retry because a rare duplicate SMS is better than a lost OTP, but many providers accept a client reference ID that lets them dedup too. For push, FCM collapse keys and APNs apns-collapse-id replace earlier notifications with the same key on the device, which hides duplicates. A per-user dedup window (same template and data within 10 minutes) catches upstream bugs.

Provider integration: rate limits, retries and failover

External providers are the least reliable part. Each provider has throughput limits (FCM allows high rates but throttles bursts to a single device; SMS aggregators have TPS limits per account; SES has a per-second send rate), so workers use a distributed token bucket per provider in Redis. Retries use exponential backoff with jitter and classify errors: 4xx such as invalid token or invalid number are permanent, so drop and clean up; 429 and 5xx are retryable. After N failures a job goes to a dead-letter queue with alerting. For SMS in India, use two or three aggregators and route by live success rate per telecom operator (Jio, Airtel, Vi), because delivery quality varies by operator and time; a circuit breaker opens when a provider's error rate spikes and traffic shifts to the backup. Delivery receipts arrive later by webhook; verify their signatures, process them idempotently, and map provider message IDs back to our notification_id. Track cost per channel, since SMS and WhatsApp messages cost real money and push is free.

Preferences, quiet hours and compliance

Every send is filtered by user preferences, which makes the preference lookup the hottest read. Cache preferences in Redis with explicit invalidation on update, and keep a small default policy per category (transactional cannot be turned off, promotions can). Quiet hours: for marketing, if it is 11 pm in the user's timezone, defer to the next allowed window instead of dropping; transactional messages ignore quiet hours. Frequency caps (for example at most 3 promotional pushes per day) use Redis counters with daily TTL. India adds hard rules: commercial SMS must use DLT-registered sender IDs and templates, the rendered text must match the registered template or the operator rejects it; promotional SMS cannot be sent at night (TRAI rules restrict promotional messages to daytime hours) and must respect the national Do Not Disturb registry. Email needs one-click unsubscribe and proper SPF, DKIM and DMARC to stay out of spam, which large senders like Gmail now require. Building these checks into the platform means every product team gets compliance for free.

8 Trade-offs

Async API (202 Accepted) vs synchronous send

Synchronous sends would tie callers to provider latency and outages. Accepting after a durable write and processing async decouples producers. The cost is callers must check status or subscribe to callbacks if they care about delivery.

Kafka vs SQS/RabbitMQ for channel queues

Kafka gives high throughput, replay and ordering per key, good for campaigns and analytics. SQS or RabbitMQ give per-message acks, delays and DLQs out of the box, convenient for retries. Many teams use Kafka for ingestion and a job queue with delayed retries per channel.

Push-first with SMS fallback vs SMS always

SMS is reliable but costs money; push is free but not guaranteed. Push first, SMS fallback for important unacknowledged messages saves large costs while keeping critical messages delivered.

Central platform vs each team integrating providers

A central platform adds a dependency, but gives consistent preferences, compliance, cost tracking and provider failover. Per-team integrations duplicate this work and spam users.

9 Bottlenecks

Preference and device lookups during campaigns

50M lookups in 30 minutes. Batch reads (MGET 500 users at a time), pre-join preferences into the segment export, and cache aggressively.

Provider throughput limits

We cannot push faster than providers allow. Spread campaign sends over the window, use multiple accounts or providers, and shape traffic with token buckets.

Webhook storms

Delivery receipts for a big campaign arrive in bursts. Accept webhooks quickly into a queue and process async; aggregate stats in a stream job.

10 Failure handling

SMS provider outage

Circuit breaker opens on error rate, traffic shifts to the secondary provider, OTP latency stays within SLO. Pending retries go to the new provider.

Worker crash mid-batch

Kafka offsets are committed after jobs complete; redelivered jobs check sent-status first, so most duplicates are suppressed.

Bug sends wrong campaign

A kill switch per campaign and per template stops sends within seconds; campaigns go through a canary on 1% of the segment first.

11 What impresses

  • Physically separating transactional and marketing traffic with reserved capacity
  • Being honest that provider timeouts make exactly-once impossible, then explaining the dedup layers
  • Push-first with SMS fallback to control cost
  • Mentioning DLT templates and DND registry for Indian SMS
  • Routing SMS by per-operator success rate
  • Kill switch and canary for campaigns

12 Mistakes

  • One queue for everything, so campaigns delay OTPs
  • Calling providers synchronously in the producer's request
  • No idempotency, causing duplicate notifications on retries
  • Ignoring user preferences and frequency caps
  • Retrying permanent errors like invalid tokens forever
  • Not tracking delivery status or cost per channel

13 Follow-ups

Q1How would you guarantee OTP delivery within 10 seconds 99.9% of the time?
Q2How do you pick the best time to send a marketing push to each user?
Q3How would you support a digest that combines 10 notifications into one?
Q4How do you clean up stale push tokens?
Q5How would you show delivery analytics to product teams?
Q6How would you avoid sending a promo to a user who just uninstalled?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free