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
- 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
- "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