A notification dispatch system routes outbound notifications — email, SMS, push, in-app — to the right channels based on user preferences, respects quiet hours and frequency caps, and delivers reliably at scale. The dispatch layer sits between the event source (payment succeeded, new follower) and the delivery providers (SendGrid, Twilio, FCM), adding preference filtering, deduplication, and retry logic that would otherwise have to be implemented in every service that sends notifications.
Core Data Model
CREATE TABLE NotificationRequest (
request_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id BIGINT NOT NULL,
event_type VARCHAR(100) NOT NULL, -- 'payment.succeeded', 'new_follower'
template_id VARCHAR(100) NOT NULL,
payload JSONB NOT NULL, -- template variables
priority VARCHAR(10) NOT NULL DEFAULT 'normal', -- 'critical', 'normal', 'low'
requested_channels TEXT[] NOT NULL, -- ['email', 'push', 'sms']
idempotency_key VARCHAR(200) UNIQUE, -- prevents duplicate sends on retry
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE NotificationDelivery (
delivery_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
request_id UUID NOT NULL REFERENCES NotificationRequest(request_id),
channel VARCHAR(20) NOT NULL, -- 'email', 'push', 'sms', 'in_app'
recipient VARCHAR(500) NOT NULL, -- email address, phone, device token
status VARCHAR(20) NOT NULL DEFAULT 'pending',
-- pending, sent, delivered, failed, suppressed
provider_id VARCHAR(200), -- external message ID
attempt_count INT NOT NULL DEFAULT 0,
sent_at TIMESTAMPTZ,
failed_at TIMESTAMPTZ,
error_message TEXT
);
CREATE TABLE NotificationPreference (
user_id BIGINT NOT NULL,
event_type VARCHAR(100) NOT NULL,
channel VARCHAR(20) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
quiet_hours_start TIME,
quiet_hours_end TIME,
timezone VARCHAR(50) NOT NULL DEFAULT 'UTC',
frequency_cap INT, -- max per day, NULL = no cap
PRIMARY KEY (user_id, event_type, channel)
);
CREATE TABLE NotificationFrequency (
user_id BIGINT NOT NULL,
channel VARCHAR(20) NOT NULL,
event_type VARCHAR(100) NOT NULL,
date DATE NOT NULL,
count INT NOT NULL DEFAULT 0,
PRIMARY KEY (user_id, channel, event_type, date)
);
Dispatch Pipeline
def dispatch_notification(request_id: str):
request = db.fetchone("SELECT * FROM NotificationRequest WHERE request_id=%s", [request_id])
if not request or request['status'] != 'pending':
return
# Load user preferences for all requested channels
prefs = load_preferences(request['user_id'], request['event_type'],
request['requested_channels'])
deliveries_to_create = []
for channel in request['requested_channels']:
pref = prefs.get(channel)
# 1. Preference check
if not pref or not pref['enabled']:
log_suppression(request_id, channel, 'preference_disabled')
continue
# 2. Quiet hours check
if is_in_quiet_hours(pref):
if request['priority'] == 'critical':
pass # critical bypasses quiet hours
else:
schedule_for_after_quiet_hours(request_id, channel, pref)
continue
# 3. Frequency cap check
if pref['frequency_cap']:
daily_count = get_daily_count(request['user_id'], channel,
request['event_type'])
if daily_count >= pref['frequency_cap']:
log_suppression(request_id, channel, 'frequency_cap')
continue
# 4. Get recipient address for channel
recipient = get_channel_recipient(request['user_id'], channel)
if not recipient:
log_suppression(request_id, channel, 'no_address')
continue
deliveries_to_create.append({
'request_id': request_id,
'channel': channel,
'recipient': recipient,
})
# Create delivery records and enqueue sends
for d in deliveries_to_create:
delivery_id = create_delivery_record(d)
send_queue.enqueue(f'send_{d["channel"]}', delivery_id=delivery_id)
increment_frequency_count(request['user_id'], d['channel'],
request['event_type'])
db.execute("""
UPDATE NotificationRequest SET status='dispatched' WHERE request_id=%s
""", [request_id])
Channel Send Workers
def send_email_notification(delivery_id: str):
delivery = db.fetchone("SELECT * FROM NotificationDelivery WHERE delivery_id=%s",
[delivery_id])
request = db.fetchone("SELECT * FROM NotificationRequest WHERE request_id=%s",
[delivery['request_id']])
template = load_template(request['template_id'], 'email')
rendered = render_template(template, request['payload'])
try:
msg_id = sendgrid.send(
to=delivery['recipient'],
subject=rendered['subject'],
html=rendered['html'],
idempotency_key=str(delivery_id) # prevent duplicate sends on retry
)
db.execute("""
UPDATE NotificationDelivery
SET status='sent', provider_id=%s, sent_at=NOW(), attempt_count=attempt_count+1
WHERE delivery_id=%s
""", [msg_id, delivery_id])
except Exception as e:
db.execute("""
UPDATE NotificationDelivery
SET status='failed', error_message=%s, failed_at=NOW(), attempt_count=attempt_count+1
WHERE delivery_id=%s
""", [str(e), delivery_id])
if delivery['attempt_count'] < 3:
send_queue.enqueue_with_delay(
'send_email_notification',
delay=60 * (2 ** delivery['attempt_count']),
delivery_id=delivery_id
)
Key Interview Points
- The idempotency_key on NotificationRequest prevents duplicate sends when the caller retries a failed enqueue — same event processed twice results in one notification, not two.
- Channel suppression reasons (preference_disabled, frequency_cap, quiet_hours, no_address) are logged for debugging — “why didn’t I get a notification?” is the most common support question.
- Frequency caps prevent notification fatigue: a “someone liked your post” notification capped at 3/day means a viral post doesn’t spam the author with hundreds of push notifications.
- Critical priority notifications (security alerts, password reset, payment failure) should bypass quiet hours and frequency caps — the user needs them regardless.
- Separate queues per channel and per priority: critical email should not queue behind 10,000 low-priority marketing pushes. Use a high-priority queue for critical/transactional and a standard queue for marketing.
- Template rendering is separate from sending — render at dispatch time (captures the payload values at the moment the event occurred) rather than at send time (payload values may have changed).
Notification dispatch and multi-channel routing system design is discussed in Uber system design interview questions.
Notification dispatch and user preference filtering design is covered in Airbnb system design interview preparation.
Notification dispatch and frequency cap system design is discussed in LinkedIn system design interview guide.