How do you design a notification system?
- Technology:
- System Design
- Experience:
- Senior
Quick answer
Accept notification requests through an API, put them on a message queue, and let channel-specific workers (email, SMS, push, in-app) send them using user preferences, templates, retries with idempotency and rate limits.
Problem
Design a service that sends email, SMS, mobile push and in-app notifications for a large product.
Functional requirements
- Other services can trigger a notification for one user or a list of users.
- Support email, SMS, push and in-app channels.
- Respect user preferences and opt-outs per channel and category.
- Support templates with variables and multiple languages.
- Allow scheduled notifications and track delivery status.
Non-functional requirements
- Scalability: handle spikes such as a marketing campaign to millions of users.
- Availability: accepting requests must keep working even if a provider is down.
- Performance: transactional messages (password reset) delivered within seconds.
- Reliability: at-least-once delivery with de-duplication so users are not notified twice.
High-level architecture
- Producers call a Notification API (or publish an event). The API validates the request, stores a notification record and enqueues it.
- A routing worker loads user preferences and contact details, renders templates and fans out one message per channel onto channel-specific queues.
- Channel workers call external providers (email service, SMS gateway, APNs/FCM) and record the result. Failed sends are retried with exponential backoff and moved to a dead-letter queue after several attempts.
- In-app notifications are written to a store the client reads, and pushed in real time over WebSockets when the user is online.
Main components
- API
- Validates requests, applies authentication and stores the notification with an idempotency key.
- Message queue
- Buffers work and absorbs spikes; separate queues per channel and priority so bulk campaigns do not delay password resets.
- Workers
- Stateless consumers per channel that can be scaled independently.
- Database
- Notification records, delivery status, templates and user preferences.
- Cache
- User preferences and templates, which are read on every send.
- Rate limiter
- Per user (avoid spamming) and per provider (stay within quotas).
Database design
- notifications(id, user_id, category, payload, idempotency_key UNIQUE, created_at, scheduled_at)
- deliveries(id, notification_id, channel, status, provider_message_id, attempts, updated_at)
- user_preferences(user_id, category, channel, enabled)
- templates(id, category, channel, locale, subject, body, version)
Scaling
- Scale channel workers horizontally based on queue depth.
- Partition queues by priority; transactional messages get their own high-priority queue.
- Batch bulk sends where providers support it.
- Archive old delivery records to cheaper storage; keep recent ones for support and analytics.
Trade-offs
- At-least-once delivery is simpler and more reliable than exactly-once; idempotency keys and provider message ids handle duplicates.
- A queue adds latency and operational cost, but protects callers from slow or failing providers.
- Storing every delivery gives auditability at the cost of storage; retention limits keep it manageable.
Discussion
A notification system sends messages to users across several channels when something happens in the product: an order ships, a password changes, a comment is posted. The hard parts are reliability, not losing or duplicating messages, and respecting user preferences at scale.
The core idea is to decouple producers from delivery. Product services publish an event; the notification service decides who should be told, through which channels, and hands each delivery to a queue so slow third-party providers never block the caller.
Key points
- Decouple producers from delivery with queues
- Separate queues per channel and priority
- Retries with backoff, dead-letter queue and idempotency keys
- Check user preferences before sending
Follow-up questions
- How would you avoid sending the same notification twice?
- How do you design a rate limiter?