Compile Ready
All system design questions
System Design/Foundations

Notification Service

Design a multi-channel (push, SMS, email) notification service delivering billions of messages reliably.

Intermediate 50m interview 21m read High frequency Popularity 83
Amazon Uber LinkedIn Airbnb

Problem Statement

Design a Notification Service that can send push, email, SMS, and in-app notifications for products such as marketplaces, social networks, ride sharing apps, and enterprise SaaS. The system exposes a channel-agnostic ingestion API, accepts notification requests from many internal services, applies templates and user preferences, queues work durably, and delivers through providers such as APNs, FCM, SES, SendGrid, and SMS gateways.

At interview scale, assume billions of notifications per day, spiky campaign traffic, provider rate limits, retries, and a mix of high-priority transactional notifications and low-priority marketing notifications. The hard part is not sending one message; it is preventing duplicate sends, respecting preferences and quiet hours, isolating provider failures, tracking delivery states, and scaling fanout without letting campaigns starve transactional traffic.

The default design should optimize for durable accepted requests, asynchronous delivery, clear priority isolation, compliance, and operational control. Exact delivery is impossible because external providers and client devices are unreliable, so the system should provide at-least-once processing internally, idempotent dispatch, best-effort provider delivery, and transparent status tracking.

Business use case

Notifications drive user engagement and trust. A ride app needs trip updates and driver arrival alerts, a marketplace needs order and fraud notifications, a social network needs mentions and connection updates, and a SaaS product needs billing, security, and collaboration alerts.

The platform also gives product teams a single place to manage templates, localization, user preferences, unsubscribe rules, provider choice, analytics, and compliance. Without a shared service, every product team reimplements retry logic, rate limits, and provider integrations inconsistently.

Functional Requirements

  • Accept notification requests through a channel-agnostic ingestion API with tenant, recipient, priority, template, payload, and idempotency key.

  • Support push notifications through APNs and FCM, email through SES or SendGrid, SMS through provider gateways, and in-app delivery through WebSocket or mailbox storage.

  • Render templates with localization, variables, versioning, and channel-specific layouts.

  • Respect user preferences, subscriptions, unsubscribe choices, locale, timezone, and quiet hours.

  • Fan out high-volume campaigns to large audiences without blocking transactional notifications.

  • Deduplicate requests and delivery jobs using idempotency keys and notification identifiers.

  • Retry transient failures with exponential backoff, jitter, provider failover, and dead-letter handling.

  • Track accepted, rendered, sent, delivered, bounced, opened, clicked, unsubscribed, failed, and suppressed events for analytics and audit.

Non-Functional Requirements

Latency

The ingestion API should acknowledge durable acceptance in under 100ms to 200ms p99 for normal traffic. Transactional delivery should usually start within seconds, while marketing campaigns can be scheduled and rate-limited over minutes or hours.

Availability

Notification acceptance and transactional dispatch should target 99.99 percent availability. Provider outages must not take down ingestion; the system should queue, retry, fail over, or degrade channel by channel.

Scalability

The platform must handle billions of notifications per day and sudden spikes from campaigns, incidents, or viral events. Queues, workers, template rendering, preference reads, and provider adapters should scale horizontally and independently per channel and priority.

Durability

Once the API accepts a notification, the request and enough metadata to retry it must be durably stored before returning success. Losing accepted transactional notifications breaks user trust and can create compliance or safety incidents.

Idempotency

Clients retry API calls, queues redeliver messages, and provider calls time out. The design needs idempotency keys, dedupe windows, stable notification IDs, and provider message IDs so retries do not produce duplicate user-visible messages.

Compliance

The system must enforce unsubscribe, opt-in, GDPR deletion, retention, consent, and country-specific SMS and email rules. Compliance checks must happen before dispatch, not only during analytics cleanup.

Observability

Operators need visibility into queue lag, provider error rates, suppression reasons, retry counts, template failures, delivery receipts, and campaign progress. Analytics should be rich but should not block delivery.

Capacity Estimation

Assumptions

Assume 300M daily active users, an average of 20 logical notifications per active user per day, and a peak multiplier of 20x during campaigns, incidents, and regional spikes. That gives 6B logical notifications per day before retries.

Assume channel mix is 65 percent push, 20 percent email, 5 percent SMS, and 10 percent in-app. Average logical notification metadata and rendered payload references are 1 KB, average delivery status event is 400 bytes, and each logical notification produces 2.5 status events on average including accepted, sent, delivered or failed, and engagement events. Retries add 15 percent more provider attempts on average, but more during incidents.

Logical notifications

6B per day

300M daily active users times 20 notifications per day

Average ingestion QPS

69,500 requests per second

6B divided by 86,400 seconds

Peak ingestion QPS

1.39M requests per second

20x average peak during campaigns or incidents

Provider attempts

6.9B per day

6B logical notifications plus 15 percent retry overhead

Push volume

3.9B per day

65 percent of logical notifications

Email volume

1.2B per day

20 percent of logical notifications

SMS volume

300M per day

5 percent of logical notifications, usually the most expensive channel

Queue ingress

6 TB per day raw

6B logical jobs times 1 KB before replication and queue overhead

Status events

15B events per day

2.5 tracking events per logical notification

Tracking storage

6 TB per day raw

15B events times 400 bytes before compression, indexes, and replicas

Preference storage

2 TB raw

1B users times roughly 2 KB of preferences, subscriptions, locale, timezone, and consent metadata

Calculations

  • Logical notifications: 300M daily active users times 20 notifications per day is 6B per day.
  • Average ingestion QPS: 6B divided by 86,400 seconds is about 69,444 per second, rounded to 69,500.
  • Peak ingestion QPS: a 20x burst multiplier gives about 1.39M notifications per second.
  • Channel split: 65 percent push is 3.9B per day, 20 percent email is 1.2B per day, 5 percent SMS is 300M per day, and 10 percent in-app is 600M per day.
  • Retry overhead: if transient failures add 15 percent more attempts, 6B logical notifications become 6.9B provider attempts per day.
  • Queue ingress: 6B jobs times 1 KB is 6 TB raw per day. With replication, headers, and retention, provision several times that amount.
  • Status events: 6B logical notifications times 2.5 events each is 15B status events per day.
  • Tracking storage: 15B events times 400 bytes is 6 TB raw per day. Compression helps, but indexes and replication add overhead.
  • Preferences: 1B users times 2 KB is about 2 TB raw. Hot preference cache size depends on active users and campaign fanout patterns.

API Design

POST/api/v1/notifications

Accepts one notification request for one recipient or a small explicit recipient list. The request is channel-agnostic; downstream policy chooses eligible channels and provider adapters.

Request


{
  "tenantId": "marketplace",
  "idempotencyKey": "order-9821-shipped-user-123",
  "priority": "transactional",
  "templateId": "order_shipped",
  "locale": "en-US",
  "recipient": {
    "userId": "user_123",
    "email": "pat@example.com",
    "phone": "+14155550123",
    "deviceTokens": ["fcm_token_1"]
  },
  "channels": ["push", "email"],
  "variables": {
    "orderId": "9821",
    "trackingUrl": "https://example.com/track/9821"
  },
  "sendAfter": "2026-07-26T07:00:00Z"
}

Response


{
  "notificationId": "notif_7d4c",
  "status": "accepted",
  "deduped": false,
  "acceptedAt": "2026-07-26T07:00:00Z"
}
  • 202Accepted and durably queued
  • 400Invalid template, recipient, channel, or variables
  • 401Authentication required
  • 403Tenant is not allowed to use the requested channel
  • 409Conflicting idempotency key payload
  • 429Tenant or client rate limit exceeded
POST/api/v1/campaigns

Creates a high-volume campaign that targets an audience segment. The service stores campaign metadata and lets fanout workers expand the audience asynchronously into per-recipient jobs.

Request


{
  "tenantId": "marketplace",
  "campaignId": "summer_sale_2026",
  "priority": "marketing",
  "templateId": "sale_announcement",
  "audience": {
    "segmentId": "buyers_us_active_90d"
  },
  "channels": ["push", "email"],
  "schedule": {
    "startAt": "2026-07-27T15:00:00Z",
    "maxPerMinute": 1000000
  }
}

Response


{
  "campaignId": "summer_sale_2026",
  "status": "scheduled",
  "estimatedRecipients": 85000000
}
  • 202Campaign scheduled for asynchronous fanout
  • 400Invalid campaign, segment, schedule, or template
  • 403Tenant lacks campaign permission
  • 409Campaign ID already exists
  • 429Campaign creation limit exceeded
PATCH/api/v1/users/{userId}/notification-preferences

Updates user-level channel preferences, subscriptions, quiet hours, locale, timezone, and unsubscribe choices. Preference changes must affect future dispatch quickly.

Request


{
  "locale": "en-US",
  "timezone": "America/Los_Angeles",
  "quietHours": {
    "start": "22:00",
    "end": "07:00"
  },
  "channels": {
    "push": true,
    "email": true,
    "sms": false,
    "inApp": true
  },
  "subscriptions": {
    "marketing": false,
    "orderUpdates": true,
    "security": true
  }
}

Response


{
  "userId": "user_123",
  "status": "updated",
  "updatedAt": "2026-07-26T07:01:00Z"
}
  • 200Preferences updated
  • 400Invalid preference shape
  • 401Authentication required
  • 403Caller cannot update this user
  • 404User not found
GET/api/v1/notifications/{notificationId}

Returns the current delivery state and attempt history for debugging, customer support, audit, or user-facing status pages.

Response


{
  "notificationId": "notif_7d4c",
  "status": "partially_delivered",
  "channels": [
    {
      "channel": "push",
      "provider": "fcm",
      "status": "delivered",
      "providerMessageId": "projects/app/messages/123"
    },
    {
      "channel": "email",
      "provider": "ses",
      "status": "deferred",
      "nextRetryAt": "2026-07-26T07:05:00Z"
    }
  ]
}
  • 200Status returned
  • 401Authentication required
  • 403Caller cannot access this notification
  • 404Notification not found
POST/api/v1/provider-receipts/{provider}

Receives provider callbacks for delivery, bounce, complaint, open, click, unsubscribe, and token invalidation events. The endpoint validates signatures and writes events to the tracking pipeline.

Request


{
  "providerMessageId": "provider_msg_123",
  "eventType": "delivered",
  "occurredAt": "2026-07-26T07:02:00Z",
  "recipient": "pat@example.com",
  "metadata": {
    "smtpResponse": "250 OK"
  }
}

Response


{
  "status": "accepted"
}
  • 202Receipt accepted
  • 400Invalid receipt payload
  • 401Invalid provider signature
  • 404Unknown provider route
DELETE/api/v1/users/{userId}/notification-data

Starts GDPR deletion or anonymization for notification history, device tokens, and preference data where legally allowed. Some audit records may be retained with personal data removed.

Response


{
  "userId": "user_123",
  "status": "deletion_requested",
  "requestedAt": "2026-07-26T07:03:00Z"
}
  • 202Deletion workflow started
  • 401Authentication required
  • 403Caller cannot delete this user's data
  • 404User not found

The API acknowledges durable acceptance, not final delivery. Delivery is asynchronous because provider latency, quiet hours, retries, and user device availability are outside the request path. Use idempotency keys on ingestion and stable provider receipt IDs on callbacks.

Database Design

The serving model separates durable notification intent from per-channel delivery attempts. A notification request captures who should be notified, why, with which priority and template. Delivery attempts capture each channel/provider try and can be retried independently.

Preferences and templates are read on the dispatch path, so they need cache-friendly keys and fast invalidation. High-volume tracking events should flow to an append-only event store and analytics pipeline, while relational or key-value tables keep the latest status and operational metadata.

notification_requests
notification_iduuidPrimary key generated by the service
tenant_idvarchar(64)Product or business unit sending the notification
idempotency_keyvarchar(256)Client-provided dedupe key scoped by tenant
priorityvarchar(32)Transactional, high, normal, or marketing
template_idvarchar(128)Logical template name
recipient_refvarchar(256)User ID or audience member reference
channelsjsonRequested channels and fallback order
payload_refvarchar(512)Pointer to encrypted variables or payload blob
statusvarchar(32)Accepted, rendering, queued, sent, partial, suppressed, failed
created_attimestampAcceptance time
expires_attimestamp nullableDeadline after which delivery should stop
delivery_attempts
attempt_iduuidPrimary key for a single channel/provider attempt
notification_iduuidParent notification request
user_idvarchar(128)Recipient, nullable for external contacts if policy allows
channelvarchar(32)Push, email, sms, or in_app
providervarchar(64)FCM, APNs, SES, SendGrid, Twilio, or internal
provider_message_idvarchar(256) nullableProvider receipt correlation key
attempt_numberintRetry count starting at one
statusvarchar(32)Queued, sent, delivered, bounced, throttled, failed, dead_lettered
scheduled_attimestampWhen this attempt becomes eligible to run
sent_attimestamp nullableWhen the provider accepted the attempt
last_error_codevarchar(128) nullableProvider or internal error code
user_preferences
user_idvarchar(128)Primary key
tenant_idvarchar(64)Tenant scope for multi-tenant products
localevarchar(16)Preferred language and region
timezonevarchar(64)Used for quiet hours and scheduling
channel_preferencesjsonPer-channel opt-in and opt-out settings
subscriptionsjsonTopic and notification-type subscriptions
quiet_hoursjsonLocal time windows for non-urgent notifications
consent_versionvarchar(64)Latest consent policy accepted by the user
gdpr_deleted_attimestamp nullableSet when personal data must no longer be used
updated_attimestampCache invalidation version source
templates
template_idvarchar(128)Logical template name
tenant_idvarchar(64)Owner tenant
versionintImmutable version number
localevarchar(16)Language and region variant
channelvarchar(32)Push, email, sms, or in_app
subjecttext nullableEmail subject or push title
body_urivarchar(512)Pointer to rendered template body in object storage or CMS
variables_schemajsonAllowed variables and validation rules
statusvarchar(32)Draft, active, deprecated, blocked
updated_attimestampVersion cache invalidation timestamp

Indexes

  • notification_requests.tenant_id, idempotency_key must be unique for the idempotency window.
  • notification_requests.created_at and tenant_id, created_at support audit and operational queries.
  • delivery_attempts.notification_id supports status lookup for one notification.
  • delivery_attempts.provider_message_id supports provider receipt correlation.
  • delivery_attempts.status, scheduled_at supports retry workers finding due attempts if the queue needs repair.
  • user_preferences.user_id is the primary read key; updated_at drives cache invalidation.
  • templates.template_id, tenant_id, locale, channel, version supports deterministic template lookup.

Relationships

One notification request can create multiple delivery attempts, one per selected channel and retry. User preferences influence whether attempts are created or suppressed. Templates are immutable by version so in-flight notifications can be audited against the exact content that was rendered.

NoSQL alternatives

At high scale, keep recent notification status in a wide-column or key-value store keyed by notification_id and partitioned by tenant. Store delivery attempts in an append-friendly table keyed by notification_id plus attempt_id, with a secondary lookup by provider_message_id for receipts.

Queues hold the operational source of work. Analytics events belong in Kafka, Kinesis, Pub/Sub, or another log, then flow to OLAP storage such as ClickHouse, Druid, BigQuery, or Snowflake. User preferences can live in DynamoDB, Cassandra, Spanner, or a sharded relational store with Redis in front for hot reads during fanout.

High-Level Architecture

Drag to pan · Ctrl/⌘ + scroll to zoom

The critical path is durable acceptance into priority queues. Delivery then happens asynchronously through fanout workers, dispatch workers, channel adapters, and external providers, while tracking events flow to analytics.

The architecture deliberately separates ingestion from delivery. The Notification API authenticates callers, validates templates and payloads, checks idempotency, persists the request, and enqueues work into priority queues. This lets callers retry safely and prevents slow providers from blocking product services.

Priority queues isolate transactional traffic from marketing campaigns. Fanout workers expand large audiences into per-recipient jobs at controlled rates, while dispatch workers apply preferences, quiet hours, localization, dedupe, provider quotas, and retry policy. Channel adapters hide provider-specific APIs and normalize receipts.

Tracking and analytics are asynchronous. They provide dashboards, delivery status, provider health, and campaign metrics, but delivery should not depend on analytics storage being healthy.

Request Flow

  1. 1

    Client submits a notification

    An internal product service calls POST /api/v1/notifications with tenant, recipient, template, variables, requested channels, priority, and an idempotency key. The API gateway authenticates the caller and applies coarse tenant quotas.

  2. 2

    API validates and deduplicates

    The Notification API validates the template ID, payload schema, recipient fields, priority, and allowed channels. It checks the idempotency key scoped by tenant. If the same key and payload were already accepted, it returns the original notification ID instead of creating duplicate work.

  3. 3

    Request is persisted and enqueued

    The service writes the notification request and minimal payload reference durably, then publishes a job to the correct priority queue. The client receives 202 Accepted after the queue write is durable, not after provider delivery.

  4. 4

    Campaign fanout expands audiences

    For campaigns, fanout workers read the audience segment in shards, apply coarse suppression rules, and create per-recipient jobs gradually. They respect campaign send rate, tenant budgets, and queue backpressure so a huge campaign does not flood workers.

  5. 5

    Dispatch worker applies policy

    A dispatch worker consumes a delivery job, reads user preferences and template version, checks unsubscribe and quiet hours, chooses channels, and either suppresses, schedules for later, or proceeds to rendering.

  6. 6

    Template is rendered and localized

    The worker renders channel-specific content using locale fallback, validates required variables, enforces size limits, and strips unsafe content. Rendering failures are recorded as terminal or retryable depending on the cause.

  7. 7

    Provider adapter sends the message

    The worker calls a normalized channel adapter. The adapter enforces per-provider token buckets, chooses a healthy provider, sends the request, records the provider message ID, and returns accepted, retryable failure, or terminal failure.

  8. 8

    Retries and failover are scheduled

    Transient errors such as timeouts, throttling, or 5xx responses are retried with exponential backoff and jitter. If the primary provider is unhealthy, the adapter can fail over to a secondary provider when compliance, cost, and message semantics allow it.

  9. 9

    Receipts and analytics update state

    Providers later send delivery, bounce, complaint, token invalidation, open, click, or unsubscribe callbacks. The receipt endpoint validates signatures, emits tracking events, updates latest status, invalidates dead device tokens, and feeds analytics dashboards.

Core Components

Notification API

Accepts channel-agnostic notification requests and makes them durable.

This stateless API validates callers, schemas, templates, priorities, channels, and idempotency keys. It persists accepted requests, enqueues jobs, and returns a stable notification ID. It should not call external providers synchronously.

Preference and Template Store

Stores user policy and reusable content definitions.

Preferences include channel opt-ins, subscriptions, quiet hours, locale, timezone, consent, and GDPR deletion flags. Templates are immutable by version and vary by tenant, locale, and channel. Both are heavily cached because fanout can create massive read bursts.

Priority Queues

Buffer accepted work and isolate traffic classes.

Use separate queues or weighted partitions for transactional, high-priority, normal, and marketing traffic. Queue metadata should include due time, priority, tenant, channel, attempt number, and dedupe identifiers. Delayed queues or scheduled topics support quiet hours and backoff.

Fanout Workers

Expand campaigns and audience segments into per-recipient delivery jobs.

Fanout workers shard audience scans, apply tenant and campaign send rates, skip clearly ineligible users, and enqueue per-recipient jobs gradually. They checkpoint progress so a worker crash does not restart an entire 100M recipient campaign from the beginning.

Dispatch Workers

Apply policy, render content, and execute retry decisions.

Dispatch workers consume queue jobs, load preferences and templates, enforce compliance, render content, choose channels, and call adapters. They are scaled independently by priority and channel so SMS throttling does not slow push or email.

Channel Adapters

Normalize provider APIs and hide channel-specific details.

Adapters encapsulate APNs, FCM, SES, SendGrid, SMS providers, and in-app/WebSocket delivery. They translate payload formats, manage provider credentials, enforce provider quotas, map provider errors into common categories, and correlate provider message IDs with internal attempts.

Dedupe and Rate Limiter

Prevents duplicate sends and protects users and providers.

Redis or a similar low-latency store maintains idempotency keys, per-user notification budgets, per-tenant quotas, per-provider token buckets, and short-lived suppression records. Durable tables backstop longer dedupe windows when needed.

Tracking and Analytics Pipeline

Collects status events, receipts, and campaign metrics.

The pipeline ingests accepted, rendered, suppressed, sent, delivered, bounced, opened, clicked, and unsubscribed events. It updates latest status for support use cases and aggregates metrics for product teams without blocking dispatch workers.

Deep Dive

Channel-agnostic ingestion versus channel-specific dispatch

A clean design accepts a logical notification intent first: who should receive something, why, how urgent it is, which template to use, and which channels are allowed. This lets product services stay independent of APNs payload rules, email headers, SMS character limits, and WebSocket session state.

Dispatch is where channel-specific behavior belongs. Push payloads need device tokens, platform-specific fields, collapse keys, and token cleanup. Email needs subject, HTML and text bodies, bounce handling, complaint handling, and unsubscribe headers. SMS needs strict length control, country rules, sender IDs, and cost controls. In-app needs mailbox persistence or online WebSocket delivery.

The boundary is important in interviews. If callers send provider-specific payloads directly, the platform cannot centralize preference enforcement, localization, fallback, analytics, or provider failover. If the core API hides every channel detail, it still needs extensible channel options for legitimate provider features.

Fanout for high-volume campaigns

Campaigns are dangerous because one API call can create tens or hundreds of millions of recipient jobs. Do not enqueue all recipients in one transaction or scan the audience from a single worker. Store campaign metadata, split the audience into shards, and let fanout workers checkpoint progress per shard.

Fanout should be paced by campaign budget, tenant quota, queue lag, provider quota, and time windows. A campaign for 100M users may need to run over hours to avoid provider throttling and user fatigue. Transactional queues must have reserved worker capacity so password reset or order updates are not stuck behind marketing sends.

Deduplication is also a fanout concern. The same user may appear in multiple segments or devices. Use campaign_id plus user_id plus notification_type as a dedupe key when product semantics require one message per user.

Preferences, subscriptions, quiet hours, and compliance

Preference enforcement must happen close to dispatch because user choices can change after ingestion but before delivery. The worker should read the current preference version and evaluate channel opt-in, topic subscription, quiet hours, timezone, age restrictions, country rules, and GDPR deletion flags.

Quiet hours are not always terminal suppression. A marketing push can be rescheduled until the next allowed local window, while a security alert or delivery driver arrival may bypass quiet hours based on policy. The request should include priority and notification type so the policy engine can make that distinction.

Unsubscribe and consent rules need special care. Email marketing must honor unsubscribe headers and suppression lists. SMS often needs explicit opt-in and STOP handling. GDPR deletion should remove or anonymize personal data from preferences, device tokens, and historical analytics according to retention policy.

Idempotency, dedupe, and exactly-once myths

Exactly-once delivery to users is not realistic. Clients retry API calls, queues redeliver messages, workers crash after provider calls, provider responses time out, and devices may show or drop notifications unpredictably. The practical goal is at-least-once internal processing with idempotent side effects and strong duplicate suppression.

On ingestion, use tenant_id plus idempotency_key to return the same notification_id for retried requests. On dispatch, use notification_id plus channel plus recipient plus attempt semantics to avoid sending the same user-visible message twice. If a provider call times out after the provider accepted it, the adapter should correlate provider_message_id when available and apply cautious retry policy.

Dedupe windows differ by use case. Password reset may allow a short resend window, order shipped should dedupe per order state transition, and daily digest should dedupe per day. The design should make dedupe keys explicit rather than guessing from rendered content.

Rate limiting, retries, backoff, and provider failover

There are multiple rate limits: per-user to prevent spam, per-tenant to enforce fairness, per-campaign to smooth fanout, and per-provider to stay within APNs, FCM, email, or SMS quotas. Token buckets in Redis work well for fast decisions, backed by configuration stored durably.

Retries should be based on error class. Retry timeouts, 429, and 5xx with exponential backoff and jitter. Do not retry invalid tokens, unsubscribed recipients, malformed payloads, or permanent bounces. Put attempts that exceed retry limits into a dead-letter queue with enough context for replay after fixes.

Failover is not always safe. Email can often fail over from SES to SendGrid if sender reputation, DKIM, and unsubscribe state are aligned. SMS failover may change sender identity or cost. Push failover is limited because APNs and FCM are platform-specific. A good answer describes channel-specific failover constraints.

Delivery tracking and analytics

A notification has multiple states. Accepted means the platform durably received the request. Sent means a provider accepted an API call. Delivered usually means the provider or device reported delivery, which is not available for every channel. Opened and clicked are engagement events and may be sampled or delayed.

Track events append-only, then compute latest status and aggregates asynchronously. This supports audit and analytics without turning the dispatch path into a transactional analytics workflow. Correlate events using notification_id, attempt_id, provider_message_id, user_id, campaign_id, channel, provider, and template version.

Analytics must tolerate duplicates and late arrivals because providers may retry webhooks. Use receipt IDs or provider message IDs for dedupe, watermark late events, and surface freshness in dashboards. For privacy, avoid storing raw IPs or full message bodies in analytics unless necessary.

Scaling

Prototype: one service and one queue

Start with a Notification API, a relational database, one durable queue, a small worker fleet, and one provider per channel. Implement idempotency, templates, preferences, and basic status tracking from the beginning because retrofitting them after duplicate sends is painful.

Growth: separate channels and priorities

Split queues and workers by priority and channel. Add Redis for idempotency, preference caching, and rate limiting. Add provider adapters, retry queues, dead-letter queues, and dashboards for queue lag, send rate, and error categories.

Large scale: campaign fanout and distributed stores

Introduce campaign metadata, sharded fanout workers, checkpointed audience scans, per-tenant quotas, and distributed storage for preferences and status. Keep transactional capacity reserved and use backpressure so marketing work expands only as fast as downstream queues can absorb it.

Global scale: regional dispatch and provider routing

Run ingestion and dispatch in multiple regions. Route users to nearby regions for in-app/WebSocket and route provider traffic based on channel, geography, compliance, and provider health. Replicate preferences and templates globally with versioning and clear consistency expectations.

Extreme scale: automated control plane

Add automated provider health scoring, adaptive rate limits, campaign pacing, anomaly detection, template safety checks, privacy workflows, and self-service tenant controls. At this stage, operating the platform safely is as important as raw send throughput.

Bottlenecks & Optimizations

Campaign fanout overwhelms queues

Shard audience scans, checkpoint fanout progress, pace per campaign, and use queue backpressure. Reserve capacity for transactional queues and pause or slow marketing fanout when queue lag or provider throttling rises.

Provider rate limits and throttling

Maintain per-provider and per-region token buckets, smooth traffic, classify retryable errors, and add jitter. Use secondary providers only when sender identity, compliance, and content semantics remain valid.

Preference store hot reads during large fanout

Cache preferences by user and version, batch reads by shard, prefetch hot segments, and keep preference records small. Use invalidation streams so recent unsubscribe or GDPR updates reach dispatch quickly.

Template rendering CPU and payload size

Precompile templates, cache active versions, validate schemas at publish time, enforce size limits per channel, and scale rendering workers independently. Reject or quarantine templates that create invalid provider payloads.

Retry storms during provider incidents

Use exponential backoff, jitter, circuit breakers, retry budgets, and dead-letter queues. When a provider is down, slow new attempts instead of allowing every worker to retry aggressively.

High-cardinality analytics writes

Append raw events to a log, aggregate asynchronously by tenant, campaign, channel, provider, and time bucket, and use sampling or approximate sketches where product requirements allow. Do not synchronously update dashboards from dispatch workers.

Failure Handling

Primary email or SMS provider outage

Open a circuit breaker for the failing provider, slow or pause attempts, and route eligible traffic to a secondary provider. Preserve provider-specific compliance rules and avoid failover if it would violate opt-in, sender identity, or regional policy.

Queue backlog grows rapidly

Autoscale workers, prioritize transactional queues, slow campaign fanout, shed low-priority marketing work if allowed, and expose delayed delivery status. Backpressure should reach campaign creation before storage or providers are overwhelmed.

Preference store or cache is unavailable

Use short-lived cached preferences when safe, fail closed for marketing, and allow critical transactional notifications only if policy permits. Do not send messages that may violate unsubscribe or GDPR flags because preference data is unavailable.

Template bug affects a live campaign

Version templates immutably, canary new templates, validate required variables before activation, and keep a kill switch to pause a campaign or block a template version. Already queued jobs should reference the exact version used for audit.

Worker crashes after provider accepted a send

Record attempt state before and after provider calls, use provider_message_id when available, and retry cautiously with idempotency metadata. Reconciliation jobs can query providers or process receipts to repair uncertain states.

Receipt endpoint receives duplicate or late callbacks

Validate signatures, dedupe by provider receipt ID or provider_message_id plus event type and timestamp, and process events idempotently. Late receipts should update analytics while respecting state transition rules.

Security

Authentication and tenant isolation

Only authorized services can send notifications for a tenant, template, or channel. Enforce tenant-scoped quotas, template ownership, provider credentials, and access to status or analytics data.

PII protection

Recipients, phone numbers, emails, device tokens, and template variables are sensitive. Encrypt payloads at rest, minimize what is written to logs, use access controls for support tools, and redact personal data in analytics where possible.

Unsubscribe, consent, and GDPR

Honor unsubscribe and STOP events quickly, store consent state with versioning, and delete or anonymize personal data according to GDPR workflows. Marketing should fail closed when consent state is uncertain.

Template and content safety

Validate templates at publish time, restrict unsafe HTML, prevent header injection in email, validate URLs, and apply tenant-specific branding rules. Template variables should be escaped according to channel.

Provider credential security

Store provider credentials in a secret manager, rotate keys, scope credentials per tenant or environment, and audit adapter access. Do not expose provider tokens to product services or client applications.

Abuse and spam prevention

Apply per-user, per-tenant, and per-channel limits, detect suspicious campaigns, and support manual or automated campaign review. SMS and email abuse can damage sender reputation and create direct cost.

Tradeoffs

Pros

  • +Asynchronous queues decouple product services from slow or failing providers.
  • +Priority isolation protects transactional notifications from marketing campaigns.
  • +Centralized templates, preferences, and compliance reduce duplicate logic across teams.
  • +Provider adapters make failover and observability consistent across channels.
  • +Append-only tracking supports analytics, audit, and debugging without blocking delivery.

Cons

  • Final delivery is eventually consistent and cannot be guaranteed for every channel.
  • The platform has significant operational complexity around queues, retries, quotas, and providers.
  • Preference and compliance reads can become expensive during large fanout.
  • Provider failover can affect cost, sender reputation, and user experience.
  • Strong dedupe across all retries and provider uncertainty is difficult to make perfect.

Alternatives

Alternative one is direct provider integration from each product service. It is simple initially but leads to inconsistent templates, retries, compliance, and analytics.

Alternative two is a managed vendor such as Braze, Iterable, Customer.io, or Firebase for most messaging needs. This reduces engineering effort but may limit customization, data control, cost optimization, and deep product integration.

Alternative three is a pure event-bus model where product events are consumed by notification rules. This is powerful for automation, but it still needs the same dispatch, preference, dedupe, and provider layers underneath.

When not to use this design

Do not build a full notification platform for a small product with one low-volume channel and no compliance complexity. A direct provider integration or managed notification tool is usually better until the organization needs shared templates, preference enforcement, fanout, multi-channel delivery, and operational analytics.

Follow-up Questions

Why should ingestion return 202 Accepted instead of waiting for delivery?

External providers and devices can be slow or unavailable, and quiet hours may delay delivery intentionally. Returning 202 after durable enqueue gives callers a reliable contract while allowing asynchronous retries, scheduling, and provider failover.

How do you prevent duplicate notifications when clients retry?

Require tenant-scoped idempotency keys on ingestion and store the original notification ID and payload hash for a dedupe window. If the same key and payload arrives again, return the original result. If the same key has a different payload, return a conflict.

How should quiet hours work?

Quiet hours should be evaluated in the user's timezone at dispatch time. Low-priority notifications can be delayed until the next allowed window, while security or transactional alerts may bypass quiet hours according to product policy and legal rules.

How do you handle a provider timeout after sending?

Treat the state as uncertain. If the provider supports idempotency or a provider message ID, use it to reconcile. Otherwise retry cautiously based on notification type and duplicate tolerance. For low-duplicate-tolerance channels such as SMS, prefer reconciliation or delayed retry over aggressive resend.

How do you keep campaigns from starving transactional notifications?

Use separate queues, reserved worker pools, weighted scheduling, campaign pacing, and backpressure. Transactional queues should have strict latency SLOs and capacity reservations, while marketing queues can be delayed or paused.

Which delivery states are reliable?

Accepted and queued are internal and reliable if the system is durable. Sent means the provider accepted the request. Delivered, opened, and clicked depend on provider and client behavior, so they are eventually consistent and sometimes unavailable.

Where should unsubscribe be enforced?

Unsubscribe should be enforced before dispatch and updated from provider receipts quickly. Email and SMS adapters should also add required unsubscribe metadata, but the core policy engine should suppress ineligible notifications before provider calls.

Company Variations

Amazon

Amazon interviewers often emphasize operational excellence, DynamoDB-style scaling, SES integration, per-tenant quotas, alarms, cost controls, and blast-radius isolation. Be ready to explain durable queues, provider throttling, and how transactional notifications survive campaign spikes.

Uber

Uber may frame this around trip lifecycle alerts, driver and rider push/SMS, regional failover, and low-latency transactional delivery. Discuss priority isolation, SMS fallback for critical trip events, mobile push token management, and provider degradation during regional incidents.

LinkedIn

LinkedIn tends to probe feed, messaging, email digests, connection requests, and notification fatigue. Emphasize preference modeling, relevance, batching, digests, unsubscribe compliance, and analytics for opens, clicks, and suppressions.

Airbnb

Airbnb may focus on host and guest booking flows, trust and safety messages, localization, timezone-aware quiet hours, and global SMS/email reliability. Explain template localization, transactional versus promotional policy, and fallback when travelers are offline.

Interview Tips

Start by clarifying notification types, channels, and delivery guarantees. Then draw the ingestion path and explicitly say the API returns after durable enqueue, not after delivery. From there, layer in priority queues, fanout, policy checks, rendering, provider adapters, retries, receipts, and analytics. Keep returning to the core tradeoff: fast reliable acceptance and controlled asynchronous delivery are more realistic than pretending every provider send is immediate and exactly once.

What interviewers expect

  • Draw ingestion, durable queues, fanout, dispatch workers, provider adapters, and analytics separately.
  • Explain priority queues and reserved capacity for transactional notifications.
  • Compute average and peak QPS, queue volume, retry overhead, and tracking event volume.
  • Discuss preferences, quiet hours, localization, and template versioning.
  • Describe idempotency keys, dedupe windows, exponential backoff, jitter, and dead-letter queues.
  • Call out provider-specific constraints for push, email, SMS, and in-app delivery.

Common mistakes

  • !Calling APNs, FCM, email, or SMS providers synchronously from the ingestion API.
  • !Mixing transactional and marketing traffic in one queue with no priority isolation.
  • !Ignoring preferences, unsubscribe, quiet hours, and GDPR until after dispatch.
  • !Promising exactly-once user delivery instead of idempotent at-least-once processing.
  • !Retrying permanent provider failures such as invalid tokens or unsubscribed recipients.

Red flags

  • ×No capacity math for billions of notifications per day and spiky campaigns.
  • ×No dedupe or idempotency strategy for client retries and queue redelivery.
  • ×No plan for provider throttling, failover, or circuit breakers.
  • ×No distinction between accepted, sent, delivered, opened, and clicked states.
  • ×No compliance story for unsubscribe, consent, SMS opt-in, or data deletion.

Revision Notes

  • The Notification API should accept channel-agnostic requests, validate idempotency, persist intent, enqueue work, and return 202 Accepted.
  • At 300M daily active users and 20 notifications each, plan for 6B logical notifications per day, about 69,500 average ingestion QPS, and about 1.39M peak QPS at 20x.
  • Separate transactional and marketing queues. Reserve capacity for urgent notifications and pace campaigns with backpressure.
  • Fanout workers expand large audiences gradually and checkpoint progress. Do not enqueue 100M recipient jobs in one synchronous request.
  • Dispatch workers enforce preferences, subscriptions, quiet hours, locale, consent, GDPR flags, and channel policy before provider calls.
  • Templates should be versioned and localized. Store enough version metadata to audit exactly what was sent.
  • Use idempotency keys at ingestion and dedupe keys at dispatch. Exactly-once user delivery is not realistic.
  • Retry transient failures with exponential backoff and jitter. Do not retry permanent bounces, invalid tokens, malformed payloads, or unsubscribed recipients.
  • Provider adapters normalize APNs, FCM, email, SMS, and in-app delivery, manage quotas, and map provider errors into common categories.
  • Track accepted, queued, sent, delivered, bounced, opened, clicked, unsubscribed, suppressed, failed, and dead-lettered events asynchronously.
  • Compliance is part of the hot dispatch decision, not an offline reporting feature.

Flashcards

Quiz

0/7 answered

  1. 1.What should the ingestion API return after it durably stores and queues a notification?

  2. 2.What is the best way to protect transactional notifications during a huge marketing campaign?

  3. 3.Which provider result should usually be treated as terminal rather than retried?

  4. 4.Why are idempotency keys required on notification ingestion?

  5. 5.Where should unsubscribe and quiet-hour checks happen?

  6. 6.What is the safest retry pattern for transient provider failures?

  7. 7.What does a provider delivery receipt usually mean?

Cheat Sheet

Goal: accept channel-agnostic notification requests, queue them durably, apply templates and preferences, deliver through push, email, SMS, and in-app channels, and track outcomes.

Scale: 300M daily active users times 20 notifications per day gives 6B logical notifications per day. Average ingestion is about 69,500 QPS, with 20x peaks around 1.39M QPS. Retry overhead can add 15 percent or more provider attempts.

API contract: return 202 Accepted after durable persistence and enqueue. Use tenant-scoped idempotency keys and stable notification IDs. Do not wait for provider delivery in the request path.

Queues: split by priority and channel. Reserve capacity for transactional notifications. Use delayed queues for quiet hours and retry backoff. Use dead-letter queues for exhausted or poisoned jobs.

Fanout: campaign creation stores metadata. Fanout workers shard audience scans, checkpoint progress, dedupe recipients, pace delivery, and emit per-recipient jobs gradually.

Policy: dispatch workers check preferences, subscriptions, unsubscribe, consent, timezone, quiet hours, GDPR flags, and notification type before rendering or sending.

Templates: version templates by tenant, locale, and channel. Validate variables at publish and render time. Enforce channel size limits and escaping rules.

Providers: adapters normalize APNs, FCM, SES, SendGrid, SMS, and in-app/WebSocket. Enforce per-provider token buckets, classify errors, handle credentials, and support cautious failover.

Retries: retry transient errors with exponential backoff and jitter. Do not retry invalid tokens, permanent bounces, unsubscribed users, malformed payloads, or policy suppressions.

Tracking: append accepted, queued, rendered, suppressed, sent, delivered, bounced, opened, clicked, unsubscribed, failed, and dead-lettered events. Update latest status and analytics asynchronously.

Compliance: unsubscribe, opt-in, STOP, GDPR deletion, retention, and audit are first-class requirements. Marketing should fail closed if consent is uncertain.

References