Notification Preferences System Low-Level Design

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: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Atlassian Interview Guide

Scroll to Top