Activity feeds and notification inboxes
Designing the "notifications" tab: activity events, per-user inbox storage, aggregation like "Alex and 12 others liked your photo", read and seen state, unread badges, fan-out and celebrity accounts, ranking and filtering, pagination, retention and cross-device sync.
Reading is half of it. See this used in a real interview: walk through Design Instagram →
Most social and collaborative products have an activity feed or notification inbox: likes, comments, follows, mentions, replies, task assignments. It looks like a simple list, but it combines fan-out, aggregation, counters and per-user state, and it is read constantly (the badge on the app icon). Instagram, Reddit and Slack designs often include it, and a notification system question may focus on it.
Events in, inbox out
- Product services emit activity events:
liked(post, actor),commented(post, actor, comment),followed(user, actor),mentioned(user, actor, message). - An activity service decides who should be notified (the post owner, mentioned users, thread participants), applies preferences and blocks, and writes an entry to each recipient's inbox.
- Optionally, it triggers a push or email for important items. See push notifications.
Events travel through a queue so spikes (a viral post receiving thousands of likes per minute) are absorbed. See event-driven architecture.
Inbox storage
A per-user, time-ordered list:
- Partition by recipient user id, sort by time (descending): a wide-column table or a sorted set for recent items. See data modelling for Cassandra and DynamoDB.
- Store references (actor ids, object ids, type) rather than copies of content; hydrate names and thumbnails at read time from caches, so changes and deletions apply everywhere.
- Keep a bounded window (say 90 days or the latest 1,000 items).
Aggregation
Users do not want 50 separate "X liked your photo" rows. Group by (recipient, type, object) within a time window:
- On write: if an aggregate for (you, like, photo 42) exists and is recent, update it (increment count, add the actor to a short list of recent actors, bump its time) instead of inserting a new row.
- Or at read: store raw events and group them when building the page.
Write-time aggregation keeps reads cheap; read-time aggregation is simpler and more flexible. Many systems aggregate on write for common types and keep the latest few actor ids for display ("Alex, Sam and 48 others").
Seen, read and unread counts
- Seen: the user opened the inbox; clears the badge. Usually a single "last seen" timestamp per user: unread count = entries newer than it.
- Read: the user tapped a specific item; stored per item (or for recent items only).
- The badge count is read on every app open and push. Keep it in a fast store (a counter per user, incremented on new entries, reset on seen), and recompute periodically to correct drift. See counting at scale.
- Sync seen and read state across devices through the server, so clearing on the phone clears on the web. See offline-first apps and sync.
Fan-out
Most activities have one or a few recipients (the post owner), so fan-out is small. Some do not:
- A popular creator posting may notify millions of followers ("X posted for the first time in a while"). Treat these like feed fan-out: batch, prioritise active users, or compute lazily when the user opens the app. See fan-out on write vs read.
- A celebrity's post receiving 100,000 likes per minute would update one aggregate row constantly: buffer and batch those updates. See hot keys and skew.
Ranking and filtering
- Tabs or filters (mentions, comments, follows) need indexes or separate lists per type.
- Some products rank notifications by importance (a reply from a close friend before a like from a stranger), with models similar to feed ranking. See recommendation systems.
- Respect blocks, mutes and privacy settings at write and read time.
Pagination
Use cursor pagination by (time, id); aggregated items move when updated, so cursors based on the aggregate's latest time can skip or repeat items. Accept small inconsistencies or snapshot the first page. See pagination.
Deletions and undo
Unlike, delete a comment, or block a user: the corresponding notification should disappear or the aggregate count should drop. Process undo events by removing the actor from the aggregate (or filtering at read time against the source of truth for small lists).
Retention and privacy
Expire old entries with TTLs, and remove entries involving deleted accounts. See privacy and data deletion.
In the interview
"Activity events go through Kafka to an activity service that resolves recipients and preferences, then upserts aggregated entries keyed by (recipient, type, object, day) into a per-user inbox table sorted by time; entries store ids hydrated from caches at read time. A per-user unread counter in Redis drives the badge, reset by a last-seen timestamp. Mass notifications from big accounts are batched for active users." See Design Instagram and Design Reddit.
Checklist
- Activity events through a queue; recipients resolved with preferences and blocks.
- Per-user inbox partitioned by user, sorted by time, storing references.
- Aggregation by recipient, type and object within a window.
- Last-seen timestamp, per-item read state, fast unread counters with periodic correction.
- Batched or lazy fan-out for large audiences; buffered hot aggregates.
- Cursor pagination, undo handling, TTL retention.