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.
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.