# Notification Preferences System Low-Level Design

Source: https://www.techinterview.org/post/3233467567/lld-notification-preferences/
Updated: 2026-04-18 · techinterview.org

## 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](https://www.techinterview.org/post/3233460278/linkedin-interview-guide-2026-social-graph-engineering-feed-ranking-and-professional-network-scale/).

Large-scale notification infrastructure is covered in [Meta system design interview preparation](https://www.techinterview.org/post/3233460272/meta-interview-guide-2026-facebook-instagram-whatsapp-engineering/).

Notification systems and preference management appear in [Amazon system design interview guide](https://www.techinterview.org/post/3233460275/scale-ai-interview-guide-2026-data-infrastructure-rlhf-pipelines-and-ml-engineering/).

See also: [Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence](https://www.techinterview.org/post/3233460274/netflix-interview-guide-2026-streaming-architecture-recommendation-systems-and-engineering-excellence/)

See also: [Atlassian Interview Guide](https://www.techinterview.org/post/3233459919/atlassian-interview/)
