Designing a Notification System
System design deep dive — multi-channel notification system with push, email, SMS, rate limiting, user preferences, and delivery guarantees.
What you'll learn
- ✓High-level architecture for a multi-channel notification system
- ✓Message queues and worker pools for reliable delivery
- ✓User preferences, rate limiting, and deduplication
- ✓Handling failures, retries, and delivery tracking
Prerequisites
- •Basic system design concepts (APIs, databases, queues)
- •Understanding of push notifications, email, and SMS
A notification system sends messages to users across multiple channels — push notifications, email, SMS, and in-app. At scale, it must be reliable, configurable, and respect user preferences. Here is how to design one.
Requirements
Functional:
- Send notifications via push, email, SMS, and in-app
- Users configure per-channel preferences
- Support for immediate and scheduled notifications
- Rate limiting to prevent spam
- Delivery tracking and analytics
Non-functional:
- High availability — notifications must not be lost
- Low latency for real-time notifications
- At-least-once delivery guarantee
- Handle 10M+ notifications per day
High-level architecture
Client → API Server → Message Queue → Workers → Delivery Services
→ Preferences DB ↓
→ Templates DB Push / Email / SMS / In-App
↓
Delivery Tracker
Components
- API Server: accepts notification requests, validates, and enqueues
- Message Queue: buffers notifications for async processing (Kafka/SQS)
- Workers: consume from queue, apply preferences, render templates, route to channels
- Delivery Services: channel-specific senders (APNs, SMTP, Twilio)
- Preferences Service: stores user notification settings
- Template Service: renders notification content from templates
API design
POST /api/notifications
{
"userId": "user-123",
"type": "order_shipped",
"data": {
"orderId": "ord-456",
"trackingUrl": "https://..."
},
"channels": ["push", "email"], // optional override
"scheduledAt": "2026-07-01T10:00:00Z" // optional
}
Message queue flow
1. API validates request
2. Fetch user preferences → filter channels
3. For each active channel, enqueue a message:
{
userId, channel, type, data, templateId, priority
}
4. Return 202 Accepted with notificationId
Using separate queues per channel allows independent scaling:
notification.push → Push Workers → APNs / FCM
notification.email → Email Workers → SES / SendGrid
notification.sms → SMS Workers → Twilio
notification.inapp → In-App Workers → WebSocket / DB
User preferences
CREATE TABLE notification_preferences (
user_id VARCHAR PRIMARY KEY,
push BOOLEAN DEFAULT true,
email BOOLEAN DEFAULT true,
sms BOOLEAN DEFAULT false,
quiet_start TIME DEFAULT '22:00',
quiet_end TIME DEFAULT '08:00',
frequency VARCHAR DEFAULT 'immediate' -- 'immediate', 'hourly_digest', 'daily_digest'
);
CREATE TABLE notification_opt_outs (
user_id VARCHAR,
type VARCHAR, -- 'order_shipped', 'marketing', etc.
channel VARCHAR, -- 'push', 'email', 'sms'
PRIMARY KEY (user_id, type, channel)
);
Rate limiting
Prevent notification fatigue:
LIMITS = {
"push": {"per_hour": 10, "per_day": 50},
"email": {"per_hour": 5, "per_day": 20},
"sms": {"per_hour": 2, "per_day": 5},
}
Use a sliding window counter in Redis:
def check_rate_limit(user_id: str, channel: str) -> bool:
key = f"notif_rate:{user_id}:{channel}"
count = redis.incr(key)
if count == 1:
redis.expire(key, 3600)
return count <= LIMITS[channel]["per_hour"]
Deduplication
Prevent duplicate notifications using an idempotency key:
def is_duplicate(notification_id: str) -> bool:
key = f"notif_dedup:{notification_id}"
return not redis.set(key, 1, nx=True, ex=86400)
Template rendering
TEMPLATES = {
"order_shipped": {
"push": {
"title": "Order Shipped!",
"body": "Your order {{orderId}} is on its way.",
},
"email": {
"subject": "Your order has shipped",
"template": "order_shipped.html",
},
},
}
def render(type: str, channel: str, data: dict) -> dict:
template = TEMPLATES[type][channel]
return {
k: v.replace("{{orderId}}", data.get("orderId", ""))
for k, v in template.items()
}
Failure handling and retries
MAX_RETRIES = 3
RETRY_DELAYS = [60, 300, 900] # 1min, 5min, 15min
def process_notification(message):
try:
send(message)
track_delivery(message, "delivered")
except TemporaryError:
retry_count = message.get("retryCount", 0)
if retry_count < MAX_RETRIES:
message["retryCount"] = retry_count + 1
requeue_with_delay(message, RETRY_DELAYS[retry_count])
else:
track_delivery(message, "failed")
alert_oncall(message)
except PermanentError:
track_delivery(message, "failed")
Delivery tracking
CREATE TABLE notification_log (
id UUID PRIMARY KEY,
user_id VARCHAR NOT NULL,
channel VARCHAR NOT NULL,
type VARCHAR NOT NULL,
status VARCHAR NOT NULL, -- 'queued', 'sent', 'delivered', 'failed'
created_at TIMESTAMP,
delivered_at TIMESTAMP,
failure_reason TEXT
);
Summary
A notification system is a pipeline: validate → check preferences → rate limit → deduplicate → render → deliver → track. Use message queues for reliability, per-channel workers for independent scaling, Redis for rate limiting and deduplication, and a delivery log for observability. Start with two channels (push + email) and add SMS and in-app as needed.
Related articles
- System Design System Design: Design an API Gateway
Design an API gateway that handles routing, authentication, rate limiting, and protocol translation. Covers plugin architecture, request pipelines, and scaling strategies.
- System Design System Design: Design a Distributed Logging System
Design a centralized logging system like the ELK stack. Covers log collection, structured ingestion, indexing, retention policies, and querying terabytes of logs efficiently.
- System Design System Design: Design a Distributed Task Scheduler
Design a distributed task scheduler that handles delayed, periodic, and one-off jobs at scale. Covers sharding, time wheels, exactly-once execution, and failure recovery.
- System Design System Design: Design an E-Commerce Checkout System
Design a checkout system that handles cart management, inventory reservation, payment orchestration, and order fulfillment. Covers saga patterns and consistency under high load.