Modelling comments and nested threads
Storing and serving comment threads at scale: flat versus nested comments, adjacency lists, materialized paths, nested sets and closure tables, loading trees efficiently, sorting by top, new and controversial, pagination of deep threads, counts, edits and deletions, and caching hot threads.
Reading is half of it. See this used in a real interview: walk through Design Reddit →
Comments look simple until they nest: Reddit threads go dozens of levels deep with thousands of replies, and each level must be sorted, paginated and counted. Even flat comments on a viral post become a hot spot. "Design Reddit" and similar questions often spend real time on how comments are stored and loaded. This guide covers the main tree models and the serving tricks.
Flat or nested
- Flat (Instagram, YouTube top level): comments belong to a post; replies are one level deep at most. Store by (post_id, sort key); paging is straightforward.
- Nested (Reddit, Hacker News): arbitrary depth. Needs a tree model and a strategy for loading subtrees.
Tree models
| Model | Stored per comment | Read a subtree | Insert | Notes |
|---|---|---|---|---|
| Adjacency list | parent_id | recursive query or several round trips | trivial | simplest; recursive CTEs make reads acceptable in SQL |
| Materialized path | path like 0001.0042.0107 | prefix query on path, one index scan | easy (parent path + own id) | very practical; path length grows with depth |
| Nested sets | left and right numbers | range query | expensive (renumbering) | good for read-mostly trees only |
| Closure table | a row per (ancestor, descendant, depth) | join on ancestor | inserts one row per ancestor | flexible queries; more storage |
For comments, the adjacency list plus a materialized path (or just a materialized path) is a strong default: inserting is cheap, and fetching a whole thread or a subtree in tree order is one indexed range scan on (post_id, path). Sorting siblings by score instead of id requires a different approach (below).
Loading threads efficiently
Large threads cannot be loaded entirely:
- Fetch the top N top-level comments by the chosen sort, then for each, its top few replies down to a limited depth (for example, 3 levels, 3 replies each), with "load more replies" links carrying cursors.
- Fetch all comments for a post in one query (by post_id) and assemble the tree in the application, if the thread is small enough (a few thousand). Many systems do this and cache the result.
- For huge threads, precompute the rendered tree for the default sort and cache it, invalidating or refreshing as votes and replies arrive. See caching.
Sorting
Each level is sorted independently:
- New: by creation time.
- Top: by score (upvotes minus downvotes, or a confidence-adjusted score).
- Best: by a lower confidence bound (Wilson score) so a comment with 9 of 10 upvotes ranks above one with 50 of 100 only when the evidence supports it.
- Controversial: high volume with balanced votes.
See voting and ranking algorithms. Scores change constantly, so sorted views are usually computed in the application or cache rather than by the database index, and refreshed periodically for hot threads.
Counts
"1.2k comments" is read far more than comments are written. Keep a counter per post (and per comment for reply counts), updated asynchronously, eventually consistent. See counting at scale.
Writes and hot threads
A viral thread may receive hundreds of comments per minute and thousands of votes:
- Write comments to a store partitioned by post_id; very hot posts can be a hot partition, so buffer writes or spread by sub-key if necessary. See hot keys and skew.
- Batch vote updates and score recomputation instead of updating sorts on every vote.
- Serve reads from a cache refreshed every few seconds; users accept slight delays in vote counts.
- Show the user's own new comment immediately (read-your-writes) even if the cache is stale. See consistency models.
Edits and deletions
- Edits update the body and set an edited flag and timestamp; keep history if moderation requires it.
- Deleting a comment with replies usually leaves a placeholder ("[deleted]") so the thread structure survives; delete fully if it has no replies.
- Moderation removals hide content but keep it for appeals and audit. See trust and safety.
Storage choices
- Relational with adjacency list and materialized path works well up to large scale, with partitioning by post. See scaling a relational database.
- Wide-column stores partitioned by post_id, clustered by path, give scalable writes and ordered subtree reads. See data modelling for Cassandra and DynamoDB.
- Document stores can embed small comment trees, but large threads exceed document limits. See document databases.
In the interview
"Comments are stored per post with parent_id and a materialized path, partitioned by post_id; the default view loads the top 50 top-level comments by 'best' and three levels of top replies, assembled in the comment service and cached for hot threads, refreshed every few seconds; deeper replies load on demand with cursors; counts and scores update asynchronously from vote events." See Design Reddit.
Checklist
- Flat or nested decided by product needs.
- Adjacency list plus materialized path (or closure table) for trees.
- Partial loading with depth and breadth limits and cursors.
- Per-level sorting with confidence-based scores; cached views for hot threads.
- Asynchronous counters; batched vote processing.
- Placeholders for deleted comments with replies; moderation kept for audit.