Mentions and Notifications System Low-Level Design

Mentions and Notifications System — Low-Level Design

A mentions system parses @username references in user content, identifies the mentioned users, and triggers notifications. It must handle at-write parsing, notification fan-out, and deduplication. This design is asked at Twitter, Slack, LinkedIn, and any platform where users can reference each other.

Parsing Mentions at Write Time

import re

MENTION_PATTERN = re.compile(r'@([a-zA-Z0-9_]{1,50})')

def extract_mentions(body):
    """Extract unique usernames from content body."""
    matches = MENTION_PATTERN.findall(body)
    return list(set(m.lower() for m in matches))  # deduplicate, normalize

def resolve_mentions(usernames):
    """Convert usernames to user IDs."""
    if not usernames:
        return []
    users = db.execute("""
        SELECT id, username FROM User
        WHERE LOWER(username) = ANY(%(names)s)
    """, {'names': usernames})
    return [u.id for u in users]

def process_content(author_id, content_id, body):
    usernames = extract_mentions(body)
    mentioned_ids = resolve_mentions(usernames)

    # Store mentions for future reference (comment edits, deletion cascades)
    for user_id in mentioned_ids:
        db.execute("""
            INSERT INTO Mention (content_id, mentioned_user_id, created_at)
            VALUES (%(cid)s, %(uid)s, NOW())
            ON CONFLICT DO NOTHING
        """, {'cid': content_id, 'uid': user_id})

    # Trigger notifications asynchronously
    if mentioned_ids:
        enqueue_mention_notifications(author_id, content_id, mentioned_ids)

Core Data Model

Mention
  id              BIGSERIAL PK
  content_id      BIGINT NOT NULL
  content_type    TEXT NOT NULL     -- 'comment', 'post', 'message'
  mentioned_user_id BIGINT NOT NULL
  created_at      TIMESTAMPTZ
  UNIQUE (content_id, content_type, mentioned_user_id)

Notification
  id              BIGSERIAL PK
  recipient_id    BIGINT NOT NULL
  actor_id        BIGINT NOT NULL   -- who triggered it
  notification_type TEXT NOT NULL   -- 'mention', 'reply', 'like', 'follow'
  content_id      BIGINT
  content_type    TEXT
  is_read         BOOLEAN DEFAULT false
  created_at      TIMESTAMPTZ
  read_at         TIMESTAMPTZ

-- Index for notification inbox
CREATE INDEX idx_notifs_recipient ON Notification(recipient_id, is_read, created_at DESC);

Notification Fan-Out Worker

def send_mention_notifications(author_id, content_id, content_type, mentioned_ids):
    author = db.get(User, author_id)
    content = db.get(content_type.capitalize(), content_id)

    for user_id in mentioned_ids:
        if user_id == author_id:
            continue  # Don't notify someone they mentioned themselves

        # Check notification preferences
        prefs = get_notification_prefs(user_id)
        if not prefs.get('mentions_enabled', True):
            continue

        # Create in-app notification
        notif = db.insert(Notification, {
            'recipient_id': user_id,
            'actor_id': author_id,
            'notification_type': 'mention',
            'content_id': content_id,
            'content_type': content_type,
        })

        # Invalidate notification count cache
        redis.delete(f'notif_count:{user_id}')

        # Push to real-time channel
        redis.publish(f'notifications:{user_id}', json.dumps({
            'type': 'mention',
            'actor': author.display_name,
            'preview': content.body[:100],
            'notification_id': notif.id,
        }))

        # Email notification (if user not online and has email enabled)
        if prefs.get('email_mentions') and not is_user_online(user_id):
            enqueue_email(
                to=db.get(User, user_id).email,
                template='mention_notification',
                vars={'actor': author.display_name, 'preview': content.body[:100]},
                idempotency_key=f'mention-notif-{notif.id}'
            )

Notification Inbox (Read/Unread)

def get_notifications(user_id, cursor=None, limit=20):
    cursor_clause = 'AND created_at < %(cursor)s' if cursor else ''
    notifications = db.execute(f"""
        SELECT n.*, u.display_name as actor_name, u.avatar_url as actor_avatar
        FROM Notification n
        JOIN User u ON n.actor_id = u.id
        WHERE n.recipient_id=%(uid)s {cursor_clause}
        ORDER BY n.created_at DESC
        LIMIT %(limit)s
    """, {'uid': user_id, 'limit': limit})
    return notifications

def get_unread_count(user_id):
    # Cache in Redis — invalidated on new notification or mark-all-read
    count = redis.get(f'notif_count:{user_id}')
    if count is not None:
        return int(count)

    count = db.execute("""
        SELECT COUNT(*) FROM Notification
        WHERE recipient_id=%(uid)s AND is_read=false
    """, {'uid': user_id}).scalar()
    redis.setex(f'notif_count:{user_id}', 300, count)
    return count

def mark_all_read(user_id):
    db.execute("""
        UPDATE Notification SET is_read=true, read_at=NOW()
        WHERE recipient_id=%(uid)s AND is_read=false
    """, {'uid': user_id})
    redis.set(f'notif_count:{user_id}', 0)

Handling Content Edits

def update_content(content_id, content_type, new_body, author_id):
    old_mentions = {m.mentioned_user_id for m in
                    db.query("SELECT mentioned_user_id FROM Mention WHERE content_id=%(id)s",
                             {'id': content_id})}
    new_usernames = extract_mentions(new_body)
    new_ids = set(resolve_mentions(new_usernames))

    added = new_ids - old_mentions
    removed = old_mentions - new_ids

    # Add new mentions
    for uid in added:
        db.execute("INSERT INTO Mention ...")
        enqueue_mention_notifications(author_id, content_id, content_type, [uid])

    # Remove stale mention records
    for uid in removed:
        db.execute("DELETE FROM Mention WHERE content_id=%(cid)s AND mentioned_user_id=%(uid)s",
                   {'cid': content_id, 'uid': uid})
    # Note: don't delete the already-sent notifications — they're historical

Key Interview Points

  • Parse at write time, not read time: Extracting mentions on every read is expensive and wasteful. Parse once on write, store in the Mention table, and use that for notifications and display.
  • Deduplication in mention processing: A comment like “Hey @alice @alice look at this” should notify @alice once. Use Python set() or UNIQUE constraint on (content_id, mentioned_user_id).
  • Notification preferences are per-user: Never send a notification type a user has disabled. Check preferences before creating the Notification row — don’t create it and then check.
  • Cache unread count, not the list: The unread count badge appears on every page load. Cache it in Redis. The full notification list is only fetched when the user opens the inbox.

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”Why parse mentions at write time instead of read time?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Read-time parsing means: for every page load of a post with 1,000 views/hour, run a regex over the body and look up each @username 1,000 times per hour. That is wasteful and slow. Write-time parsing means: run the regex once when the post is saved, resolve @usernames to user IDs, and store them in a Mention table. Subsequent reads are a simple JOIN on the Mention table — no regex, no username lookup. Additionally, write-time parsing allows immediate notification fan-out triggered by the insert, which is not possible with read-time parsing.”}},{“@type”:”Question”,”name”:”How do you handle @mentions in edited content?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”On edit, compare old and new mention sets: old_mentions = set of user IDs from Mention table for this content. new_mentions = resolve_mentions(extract_mentions(new_body)). Compute added = new – old and removed = old – new. For added users: insert new Mention rows and send notifications. For removed users: delete their Mention rows. Do NOT delete already-sent notifications — the user was notified when it was relevant. The Mention table represents the current state of the content; the Notification table is an immutable historical log.”}},{“@type”:”Question”,”name”:”How do you display @mention links in rendered content?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Store the raw body with @username text. At render time, replace @username patterns with hyperlinks using a fast regex substitution. Pre-validate that the @usernames in the rendered text are real users (use the Mention table as the source of truth — only render as links those user IDs present in the Mention table). This prevents rendering fake @mentions that point nowhere. Cache the rendered HTML with a TTL or invalidate on edit. For rich text editors (Slate, Tiptap): store @mentions as structured nodes with user_id, not just raw text, for reliable resolution.”}},{“@type”:”Question”,”name”:”How do you avoid sending duplicate notifications for the same mention?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a UNIQUE constraint on (content_id, content_type, mentioned_user_id) in the Mention table. The INSERT ON CONFLICT DO NOTHING pattern prevents duplicate Mention rows. Since notifications are triggered by the Mention insert, if the insert is a no-op (duplicate), no notification is triggered. For re-edits that re-add the same @mention: the Mention row already exists, so no new notification is sent. Only genuinely new mentions (first appearance in the content) generate notifications.”}},{“@type”:”Question”,”name”:”How do you implement notification preferences per user?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Store preferences in a NotificationPreference table: (user_id, notification_type, channel). Channels: in_app, email, push. Types: mention, reply, like, follow. Default: all enabled. The fan-out worker checks preferences before creating a Notification row or sending an email: if the user has disabled email for mentions, skip the email send. Check preferences at fan-out time (before creating the notification), not at delivery time — this avoids creating database records for notifications the user will never see.”}}]}

Mentions and notification system design is discussed in Twitter system design interview questions.

Mentions and team notification system design is covered in Atlassian system design interview preparation.

Mentions and professional notification system design is discussed in LinkedIn system design interview guide.

Scroll to Top