System DesignAdvanced

Design a notification system

“Design a service that sends email, SMS and push notifications for the rest of the company. Cover the API, the pipeline, retries, rate limits, deduplication and how you know a notification was delivered.”

What this tests

  • Queue + workers with per-channel adapters as the core shape
  • Idempotency and deduplication from producer to provider
  • Rate limiting per user and per provider, retries and dead-letter queues
  • Delivery status as a state machine with provider webhooks

Answers by level

Read the beginner answer first and notice what is missing.

Clarify: volumes (say 10 M/day ≈ 115/s average, marketing bursts of 5,000/s), channels, latency (a password-reset email within 30 s; a digest within an hour), and preferences/quiet hours. The synchronous design fails the burst and couples every caller to provider outages. The shape is API → queue → workers → channel adapters: POST /notifications {idempotency_key, user_id, template, data, channels?, priority} validates, resolves preferences, persists a notification row in queued state and enqueues. Workers pull, render the template, and call the adapter for email, SMS or push; each adapter hides a provider and its retry semantics.

Reliability is the product. Retries with backoff per attempt, a dead-letter queue per channel, and a per-provider circuit breaker. Priorities as separate queues (transactional vs marketing) so a 5 M-message campaign cannot delay a password reset. Rate limits in two places: per user (no more than N pushes per hour; respect quiet hours) and per provider (SMS providers cap throughput per account; exceeding it gets you blocked). Delivery status is a state machine: queued → sent → delivered | bounced | failed, advanced by provider webhooks, exposed via GET /notifications/{id} — see Background Jobs and Workers and Message Queues.

Green flags · Red flags

Strong green flag · Explains why a provider outage should leave messages waiting in the queue rather than flooding the DLQ, and alerts on oldest-message age.
Green flags
  • Estimates volume and separates transactional from marketing lanes
  • Queue + workers + per-channel adapters, with the provider hidden behind the adapter
  • Idempotency at caller, worker and provider levels
  • Per-user and per-provider rate limits with the reason for each
  • Delivery status as a state machine driven by webhooks, with idempotent updates
  • Retries, backoff, DLQ and a per-provider breaker
Red flags
  • "The API calls the SMS provider directly and returns the result." (no burst absorption, no isolation)
  • One queue for everything; a campaign delays password resets
  • No dedup; assumes the queue delivers exactly once
  • Cannot say how the caller learns whether the email was delivered

Follow-up questions

F1
A marketing campaign enqueues 5 M emails at 09:00. What happens to a password reset at 09:01?
F2
The worker sent the SMS and crashed before acking. Now?
F3
How do you respect "no pushes between 22:00 and 08:00"?

Scenario

The current notification service is a synchronous endpoint that calls Twilio and SendGrid inline. During a Twilio incident, checkout requests started failing because order confirmation is called from the checkout transaction; a marketing blast last week also triggered SendGrid's rate limit and blocked password resets for 40 minutes; and a retry bug sent 3,000 duplicate SMS. Redesign the system to remove all three failure modes.

Learn this topic