Skip to content

Notification System#

Problem statement (interviewer prompt)

Design a notification platform that sends 1B+ messages/day across push (iOS/Android/Web), SMS, and email. Support templates, user preferences (channel + quiet hours + frequency cap), at-least-once delivery with idempotency, and delivery/open tracking.

flowchart LR
  P[Producer Service]
  Q[(Queue)]
  W([Notification Workers])
  R[Routing<br/>by channel + prefs]
  PUSH((APNS / FCM))
  SMS[[Twilio / SNS]]
  EM((SES / SendGrid))
  USER[(User prefs / device tokens)]
  P --> Q --> W --> R
  USER -.lookup.-> R
  R --> PUSH
  R --> SMS
  R --> EM

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class P,R service;
    class Q,USER datastore;
    class SMS queue;
    class W compute;
    class PUSH,EM external;
flowchart TB
  subgraph Producers
    APP[App events]
    BATCH[Marketing batch]
    TXN[Transactional<br/>signup, OTP]
    SCHED([Scheduler<br/>reminders])
  end

  subgraph API[Notification API]
    GW[Send API<br/>POST /notify]
    TMPL[Template / Personalization<br/>Handlebars / MJML]
    DEDUP[Idempotency key]
    PRIO[Priority tier<br/>txn / promo / digest]
  end

  subgraph Queue[Async Bus]
    K1[[Kafka: txn topic]]
    K2[[Kafka: promo topic]]
    K3[[Kafka: digest topic]]
    DLQ[[Dead-letter topic]]
  end

  subgraph Router[Routing]
    PREF([User preferences<br/>opt-in / quiet hours])
    LIM([Rate limit per user<br/>throttle frequency])
    AB[A/B router]
    ROUTE[Channel selector<br/>push -> sms -> email fallback]
  end

  subgraph Channels[Channel Workers]
    PW([Push worker])
    SW([SMS worker])
    EW([Email worker])
    IW([In-app worker])
    WB([Web Push / WebSocket])
  end

  subgraph Providers
    APNS((Apple APNS))
    FCM((Google FCM))
    HMS((Huawei HMS))
    TW[[Twilio / SNS / MSG91]]
    SG((SendGrid / SES / Mailgun))
  end

  subgraph Devices
    iOS
    AND[Android]
    PH([Phone])
    EM[[Email Inbox]]
    WEB([Browser])
  end

  subgraph Storage
    PRDB[(User & Device DB<br/>tokens, prefs)]
    TPDB[(Templates<br/>versioned)]
    HIST[(History / Inbox<br/>Cassandra)]
    AUD[(Audit log)]
  end

  subgraph Feedback
    REC[Delivery receipts]
    BNC[Bounce / unsubscribe]
    ENG[Open / click events]
    ML[ML send-time optimizer]
  end

  Producers --> GW --> DEDUP --> TMPL --> PRIO
  PRIO --> K1
  PRIO --> K2
  PRIO --> K3
  K1 --> Router
  K2 --> Router
  K3 --> Router
  PREF -.lookup.-> Router
  LIM -.lookup.-> Router
  Router --> Channels
  Channels --> Providers
  Providers --> Devices
  Providers -.receipts.-> REC
  REC --> ENG --> ML --> PRIO
  Channels --> HIST
  Channels -.failure.-> DLQ
  PRDB --- Router
  TPDB --- TMPL
  Channels --> AUD
  BNC -.update prefs.-> PRDB

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class PREF,LIM,WB,PH,WEB client;
    class APP,BATCH,TXN,GW,TMPL,DEDUP,PRIO,AB,ROUTE,AND,REC,BNC,ENG,ML service;
    class PRDB,TPDB,HIST,AUD datastore;
    class K1,K2,K3,DLQ,TW,EM queue;
    class SCHED,PW,SW,EW,IW compute;
    class APNS,FCM,HMS,SG external;

Delivery guarantees#

  • At-least-once with idempotency key (tenant, event_id).
  • Receipts close the loop; without them, system can't optimize send-time.
  • Quiet hours, frequency capping per user.

Templating#

  • Source of truth in template service (versioned, localized).
  • Compile templates once; pass user data context.

Scale knobs#

  • Separate topics per priority - txn never blocked by marketing batches.
  • Per-provider client pools with circuit breakers.
  • Backoff & DLQ on provider 4xx/5xx.

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
HLD Pub/Sub & message brokers topics, consumer groups, delivery semantics pub-sub-pattern
HLD Idempotency & retries safe re-execution, backoff + jitter idempotency-retries
HLD Resilience patterns timeout, retry, breaker, bulkhead, backpressure resilience-patterns
HLD Realtime protocols WS / SSE / polling / gRPC streaming realtime-protocols
LLD REST API design verbs, statuses, pagination, errors rest-api-design
LLD Async models futures / async-await / coroutines / actors async-models

Quick reference#

Functional#

  • Multi-channel: push (iOS/Android/Web), SMS, email, in-app.
  • Personalized template rendering.
  • User preferences, opt-in, quiet hours.
  • Throttle frequency (no spam).
  • Track delivery, opens, clicks.

Non-functional#

  • 100M+ users; 1B+ notifications/day.
  • TXN delivery latency < 5 s; promo < 1 hr.
  • 99.9% delivery for txn.

Capacity#

  • 1B/day → 12k/s avg, 100k/s peak.
  • Per notification: 1-2 KB payload + 0.5 KB metadata.
  • Storage (history 90d): 1B × 90 × 2KB = 180 TB.

API#

POST /notify
{
  user_id, event_type, channel?, priority,
  template_id, params{}, idempotency_key
}

Schema#

  • users(id, locale, tz)
  • device_tokens(user_id, platform, token, last_seen)
  • prefs(user_id, channel, enabled, quiet_start, quiet_end)
  • templates(id, version, channel, locale, body)
  • history(user_id, ts, channel, status) (Cassandra)

Trade-offs#

  • Fan-out at send vs at read: notifications usually fan-out at send (push to device).
  • One queue per priority vs single queue with priority field - split is simpler.
  • Provider lock-in: use abstraction layer; APNs and FCM behave differently for receipts.
  • Order: typically not required; per-user ordering optional (use partition key = user_id).

Refs#

  • Pinterest "Notification platform" blog, Uber/Lyft notification posts, AWS Pinpoint architecture, Twilio Notify, Braze, OneSignal docs.

FAQ#

How do you design a notification system?#

Use a producer queue, stateless workers, a routing layer that joins user preferences, and per-channel adapters for APNS, FCM, SMS, and email with retries and tracking.

How do you guarantee at-least-once delivery without duplicates?#

Stamp each notification with an idempotency key derived from event ID and channel, persist a dedupe table with TTL, and short-circuit duplicate sends on retry.

How do quiet hours and frequency caps work?#

The routing layer joins the user's prefs row before send, drops or defers messages outside allowed windows, and updates a per-user counter to enforce daily caps.

Why use a message queue for notifications?#

A queue absorbs bursts, isolates failures per channel, lets you retry independently, and decouples producers from slow third-party APIs like APNS, Twilio, and SES.

How do you track delivery and opens?#

Emit delivery webhooks from APNS, FCM, and SES into a streaming pipeline, join them with the original send event, and update per-message status for dashboards and ML.

  • Pub/Sub Pattern: fan-out delivery to FCM, APNs, and email is a classic pub/sub use case
  • Rate Limiter: notification systems must rate-limit per user and per channel to avoid spam

Further reading#

Curated, high-credibility sources for going deeper on this topic.

Video walkthrough

Design a Notification System : via ByteByteGo