Phase 8 · High-level design questions · Topic 8.3

Design a notification system

Must knowHLD

Send notifications (mobile push, SMS, email) to users reliably, at large scale.

Not started

Step 1 Understand it

Why interviewers ask this. It tests , priorities, third-party integrations, and avoiding duplicates — everyday problems in real products.

Watch it happen

AnimationNotifications: one service, a queue per channel and priority
Read the animation as text
  1. "Order shipped". The order service doesn't call any provider itself: it hands one event to the notification service.
  2. The notification service checks the user's preferences (push yes, SMS no), applies per-user rate limits, and fills in the template.
  3. It puts the message on a queue per channel and per priority — sign-in codes ahead of marketing — so a flood of emails can't delay a passcode.
  4. A channel worker sends it to the push provider. The provider is slow and times out — so the worker retries with backoff.
  5. Delivered. Every status is written to a delivery log; a message that keeps failing goes to a dead-letter queue. Idempotency keys stop a retry from notifying twice.

Requirements

Functional

  • Services trigger notifications (order shipped, , marketing).
  • Channels: push, SMS, email; respect each user's preferences and opt-outs.
  • Templates, scheduling, and retries on failure.

Non-functional

  • Millions a day, with big spikes during campaigns.
  • Passcodes must arrive in seconds; marketing can be slower.
  • Delivered , without spamming duplicates.

Estimates

  • 10 million push + 1 million SMS + 5 million emails a day ≈ 200 per second on average; campaigns can be 10–100 times higher.

API

  • POST /notifications {user_id, type, template_id, data, channels?, send_at?}

Data model

  • user_preferences (user_id, channel, opted_in)
  • devices (user_id, push_token)
  • notification_log (id, user_id, channel, status, created_at)

Start simple, then scale

  1. Version 1: the order service calls the email provider directly.
  2. Version 2: a notification service with templates and user preferences.
  3. Version 3: a queue per channel, , retries and a .
  4. Version 4: priority lanes, , provider and per-user .

Picture it

flowchart LR
  P["Order, login and<br/>marketing services"] --> N["Notification service<br/>preferences · templates · limits"]
  N --> QH[["High-priority queue<br/>(passcodes)"]]
  N --> QL[["Low-priority queue<br/>(marketing)"]]
  QH & QL --> W["Channel workers"]
  W --> PUSH["Apple and Google push"]
  W --> SMS["SMS provider"]
  W --> EM["Email provider"]
  W -.->|"failed too often"| DLQ[["Dead-letter queue"]]
Messages are checked once, then queued by priority, so a marketing blast never delays a passcode. Workers deliver through outside providers.

The design, step by step

  1. A notification service checks preferences and rate limits, and fills in the template.
  2. It puts messages onto separate queues per channel (push, SMS, email) and per priority (passcodes ahead of marketing).
  3. Channel workers call the providers (Apple and Google push services, an SMS , an email service).
  4. Retries with ; repeated failures go to a dead-letter queue; every status is recorded in a .

Deep dives

  • De-duplication: an idempotency key per notification, so retries don't send twice.
  • Separate priority queues, so a marketing blast never delays a passcode.
  • Provider failover: a second SMS provider.
  • A per-user rate limit, so nobody gets spammed.

Common mistakes

  • Sending inside the user's .
  • One queue where marketing messages block passcodes.
  • Retries without , so users get duplicates.

Step 2 Go further (free, optional)

Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.

Step 3 Practise

Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.

  1. ExplainSay it in plain words in under a minute, with no notes.

    Explain the flow from 'order shipped' to a phone notification.

    Show answer

    1. The order service marks order 105 as shipped and publishes an "OrderShipped" (through its , so the event can't be lost). The notification service consumes it, or the order service calls POST /notifications, with an idempotency key such as "order-105-shipped".
    2. The notification service checks the key against its log: if this notification was already handled, it stops (no duplicates).
    3. It checks the user's preferences: is push allowed for order updates? Is the user opted out of everything? It applies a per-user rate limit.
    4. It looks up the user's active devices and their push .
    5. It fills in the template ("Your order 105 has shipped") in the user's language.
    6. It puts one message per device on the push queue for normal priority, and records "queued" in the log.
    7. A push worker takes the message and calls Apple's (APNs) or Google's (FCM) push service with the device token.
    8. The provider delivers it to the phone. The worker records "sent". If the provider says the token is no longer valid, the device is marked inactive. A temporary failure is retried with backoff; after several failures, it goes to a dead-letter queue.

  2. DrawSketch the diagram (or class diagram) from memory.

    Draw the service, the queues per channel and priority, the workers and the providers.

    Show answer

    Separate queues per channel and priority mean a marketing blast can't delay a passcode, and each worker pool can be scaled and rate-limited to match its provider. Every step updates the delivery log.

    text
    Services ── events / POST /notifications ──► Notification service
    (orders, auth, marketing)                     1. de-duplicate (key)
                                                  2. preferences, opt-outs
                                                  3. per-user rate limit
                                                  4. fill in the template
                                                     │
           ┌──────────────┬──────────────┬───────────┼───────────┐
           ▼              ▼              ▼           ▼           ▼
      push: high     push: low     SMS: high    email: high  email: low
      (passcodes)   (marketing)   (passcodes)   (receipts)  (newsletters)
           │              │              │           │           │
           ▼              ▼              ▼           ▼           ▼
      Push workers                  SMS workers   Email workers
           │                             │             │
           ▼                             ▼             ▼
      APNs / FCM                SMS gateway (+ backup)  Email provider
    
    Failures: retry with backoff ──► dead-letter queue
    Every status ──► delivery log
  3. ApplyUse it in a real design question and say exactly where it fits.

    Design the data model for preferences, devices and the delivery log, with keys.

    Show answer

    • user_preferences is keyed by (user, channel, category), so a user can turn off marketing emails but keep order texts, and security messages can be treated as always on.
    • devices has its own id, because one user has several phones; the push token is unique, and tokens the provider rejects are marked inactive rather than deleted.
    • notification_log uses the notification's id as its , which doubles as the idempotency key: retrying the same notification can't create a second row. It is by user and time for "what did we send this person?".

    At millions of rows a day, the log would be by day (and old partitions dropped after, say, 90 days), or kept in a .

    sql
    -- PostgreSQL
    CREATE TABLE user_preferences (
      user_id     BIGINT  NOT NULL,
      channel     TEXT    NOT NULL CHECK (channel IN ('push', 'sms', 'email')),
      category    TEXT    NOT NULL,          -- 'security', 'orders', 'marketing'
      opted_in    BOOLEAN NOT NULL DEFAULT TRUE,
      updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
      PRIMARY KEY (user_id, channel, category)
    );
    
    CREATE TABLE devices (
      device_id   UUID PRIMARY KEY,
      user_id     BIGINT NOT NULL,
      platform    TEXT   NOT NULL CHECK (platform IN ('ios', 'android', 'web')),
      push_token  TEXT   NOT NULL UNIQUE,
      active      BOOLEAN NOT NULL DEFAULT TRUE,     -- false once the provider rejects the token
      last_seen   TIMESTAMPTZ NOT NULL
    );
    CREATE INDEX devices_by_user ON devices (user_id) WHERE active;
    
    CREATE TABLE notification_log (
      notification_id  TEXT PRIMARY KEY,             -- also the idempotency key
      user_id          BIGINT NOT NULL,
      channel          TEXT   NOT NULL,
      template_id      TEXT   NOT NULL,
      priority         TEXT   NOT NULL,              -- 'high' or 'low'
      status           TEXT   NOT NULL,              -- queued, sent, delivered, failed, skipped
      provider_msg_id  TEXT,
      attempts         INT    NOT NULL DEFAULT 0,
      created_at       TIMESTAMPTZ NOT NULL DEFAULT now(),
      updated_at       TIMESTAMPTZ NOT NULL DEFAULT now()
    );
    CREATE INDEX log_by_user_time ON notification_log (user_id, created_at DESC);
  4. Trade-offsSay what it costs, when not to use it, and the alternative.

    One queue for everything, or separate queues per channel and priority?

    Show answer

    One queue for everything:
    • Simple: one thing to run, monitor and scale.
    • But head-of-line blocking: a 10-million-email newsletter sits in front of a one-time passcode, and the passcode arrives an hour late, so the user can't log in.
    • One slow provider (an SMS gateway limiting you to 100 a second) backs up push and email too.
    • You can't scale or rate-limit one channel without the others.

    Separate queues per channel and priority:
    • : passcodes have their own fast lane; a struggling SMS provider only slows SMS.
    • Each worker pool is sized and rate-limited to match its provider's limits.
    • Retries and dead-letter queues per channel, so failures are easier to see and fix.
    • The cost: more queues to set up and monitor, routing logic, and a rule so low priority isn't starved forever (for example, workers take 9 high-priority messages for every 1 low).

    For notifications, separate queues are clearly worth it: different channels and priorities really do have different speeds, limits and urgency.

Step 4 Interview-ready, then prove it

Your interview answer

About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.

call a notification service that checks preferences, fills in templates and puts messages on queues per channel and priority. Workers deliver through providers like Apple and Google push services and SMS gateways, with retries, dead-letter queues and idempotency keys to avoid duplicates. Passcodes get a high-priority lane, so campaigns never delay them.

Follow-up questions

How do you avoid sending the same notification twice?

An idempotency key per notification, checked in a store before sending; the worker records 'sent' only after success.

What if a provider goes down?

Retry with backoff, then fail over to a backup provider, and the failing one.

Tick each check when you can do it without notes

A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.

  • Explain Say it in plain words in under a minute, with no notes.
  • Draw Sketch the diagram (or class diagram) from memory.
  • Apply Use it in a real design question and say exactly where it fits.
  • Trade-offs Say what it costs, when not to use it, and the alternative.

0 of 4 checks ticked.

Saved in this browser. Back it up or move it to another device.