What are Notification Preferences?
Notification preferences let users control which notifications they receive, via which channel (email, push, SMS, in-app), and at what frequency. Without a preferences system, users receive every notification and churn by unsubscribing entirely. A well-designed preferences system increases engagement by letting users receive only what matters to them. LinkedIn, Slack, and Gmail all have sophisticated notification preference systems.
Requirements
- Users can toggle notification types on/off per channel (email, push, SMS, in-app)
- Support notification categories: marketing, security, social, product updates, reminders
- Frequency controls: immediate, daily digest, weekly digest
- Do Not Disturb (DND) hours: no push/SMS during 10pm-8am local time
- Unsubscribe from all for a channel (e.g., global email unsubscribe)
- Preference lookup must be fast (<5ms) since every notification send checks preferences
Data Model
NotificationCategory(category_id UUID, name VARCHAR, -- 'security_alert', 'marketing'
description VARCHAR, default_enabled BOOL,
can_disable BOOL) -- security alerts may be mandatory
UserNotificationPrefs(pref_id UUID, user_id UUID,
category_id UUID,
channel ENUM(EMAIL, PUSH, SMS, IN_APP),
enabled BOOL,
frequency ENUM(IMMEDIATE, DAILY_DIGEST, WEEKLY_DIGEST),
updated_at TIMESTAMP,
PRIMARY KEY (user_id, category_id, channel))
UserDNDSettings(user_id UUID,
dnd_enabled BOOL,
dnd_start TIME, -- e.g. '22:00'
dnd_end TIME, -- e.g. '08:00'
timezone VARCHAR, -- 'America/New_York'
channels_affected VARCHAR[]) -- ['PUSH', 'SMS']
GlobalUnsubscribe(user_id UUID, channel ENUM(EMAIL, SMS),
unsubscribed_at TIMESTAMP, reason VARCHAR)
Preference Lookup (Hot Path)
Every notification send — potentially millions per minute — must check preferences. Cache user preferences in Redis:
def should_send(user_id, category, channel):
# 1. Check global unsubscribe first
if is_globally_unsubscribed(user_id, channel):
return False, 'GLOBAL_UNSUBSCRIBE'
# 2. Load preferences (Redis cache, TTL=1h, DB fallback)
prefs = get_prefs_cached(user_id)
pref = prefs.get((category, channel))
if pref is None:
# Not set — use category default
default = get_category_default(category, channel)
return default, 'DEFAULT'
if not pref.enabled:
return False, 'USER_DISABLED'
# 3. Check DND
if channel in ('PUSH', 'SMS'):
dnd = get_dnd_settings(user_id)
if dnd.enabled and is_dnd_active(dnd):
return False, 'DND'
return True, pref.frequency
def get_prefs_cached(user_id):
key = f'notif_prefs:{user_id}'
cached = redis.get(key)
if cached:
return json.loads(cached)
prefs = db.query('SELECT * FROM UserNotificationPrefs WHERE user_id=?', user_id)
redis.setex(key, 3600, json.dumps({(p.category, p.channel): p for p in prefs}))
return prefs
Digest Aggregation
Users who choose DAILY_DIGEST should receive one email per day, not individual emails. Aggregation:
DigestItem(item_id UUID, user_id UUID, category VARCHAR, channel VARCHAR,
notification_id UUID, queued_at TIMESTAMP, sent BOOL)
When frequency=DAILY_DIGEST: instead of sending the notification, insert a DigestItem. A nightly cron job (e.g., 8am in user’s timezone) fetches all unsent DigestItems for the user, renders a digest email with all accumulated notifications, sends it, marks items sent=True. Uses user’s timezone from DND settings.
Preference Change Propagation
When a user updates preferences: write to DB, then invalidate the Redis cache key (delete, not update). The next preference lookup rebuilds the cache from DB. Do not update Redis directly — stale cache and DB divergence cause hard-to-debug bugs. For DND changes, also invalidate the DND cache key separately.
Key Design Decisions
- Redis cache for preferences — every notification send is a read; cache keeps DB load manageable
- Sparse storage — only store explicitly set preferences; missing = use category default. Avoids inserting rows for every user × category × channel combination at signup
- Global unsubscribe is a separate table — CAN-SPAM compliance requires immediate honor of unsubscribes; checked before any other logic
- DND checks timezone-aware time — compare current UTC against user’s local DND window
- Digest queue vs. send-time aggregation — queue digest items at event time, aggregate at send time to group everything in one email
Notification preferences and digest systems are discussed in LinkedIn system design interview questions.
Large-scale notification infrastructure is covered in Meta system design interview preparation.
Notification systems and preference management appear in Amazon system design interview guide.
See also: Atlassian Interview Guide