Design Reddit (posts, comments, votes)
The interviewer asks
“Design Reddit: users join communities (subreddits), submit posts, write deeply nested comments and upvote or downvote both. Pages show posts and comments sorted by hot, top, new and best.”
What a strong answer looks like
A strong answer focuses on three hard parts: votes, ranking and comment trees. Votes are high-volume, idempotent per (user, item) and must update scores without hot-row contention, so record the vote, then update scores asynchronously through a queue with batching. Listing pages (hot, top, new per subreddit and for the front page) are precomputed sorted lists in Redis, refreshed as scores change, because computing 'hot' over millions of posts per request is impossible. The hot formula combines log of net votes with post age, so new posts can compete. Comment trees are stored flat with parent_id and a materialised path, fetched for a post in one query, and assembled and sorted (by Wilson-score 'best') in the service, with deep branches truncated behind 'load more'. Numbers: ~70M DAU, ~1M votes/min peak, threads with 50k+ comments.
Concepts to mention
1 Requirements
Functional
- Create and join subreddits; moderators set rules and remove content
- Submit text, link and image posts to a subreddit
- Comment on posts and reply to comments to any depth
- Upvote or downvote posts and comments; change or remove your vote
- List posts by hot, new, top (hour/day/week/all) and rising, per subreddit and on the home feed
- Show comments sorted by best, top, new and controversial
Non-functional
- Scale: 70M DAU, ~1M posts/day, ~8M comments/day, ~100M votes/day (peak ~1M/min)
- Read heavy: ~100:1 reads to writes; listing pages p99 < 200 ms
- Vote counts may lag by a few seconds (eventual), but a user's own vote must show immediately
- One vote per user per item; resistant to vote manipulation
- Availability 99.95%; serve cached pages for logged-out users when backends are degraded
- Threads with 50k+ comments must load first page fast
Out of scope (say it out loud)
- Chat and direct messages
- Ads and Reddit Premium/awards payments
- Full-text search details
2 Estimates
3 API
| Endpoint | What it does |
|---|---|
POST/api/r/{sub}/posts | Submit a post |
GET/api/r/{sub}/{sort}?t=day&after={cursor}&limit=25 | Listing: hot | new | top | rising |
POST/api/vote | Body: item_id (post or comment), dir (+1, 0, -1); idempotent per user |
POST/api/posts/{id}/comments | Add a comment with optional parent_comment_id |
GET/api/posts/{id}/comments?sort=best&limit=200&depth=8 | Comment tree for a post, truncated with 'more' stubs |
GET/api/morechildren?post_id=&children=ids | Load a collapsed branch |
4 Data model
postsPostgres (sharded by post_id) + Memcachedpost_id (PK)subreddit_idauthor_idtitlebody/urlcreated_atupsdownsscorecomment_countremovedups/downs/score are denormalised and updated in batches by the vote pipeline.
commentsPostgres / Cassandra (partitioned by post_id)comment_id (PK)post_idparent_idpath (e.g. 0001.0042.0007)depthauthor_idbodycreated_atupsdownsPartitioning by post_id puts a whole thread in one partition, so the tree is one range read. Path gives tree order.
votesCassandra (or sharded Postgres)(user_id, item_id) PKdirectionupdated_atUpsert makes voting idempotent and lets us compute the delta when a user changes their vote. Partition by user_id for 'my votes on this page' lookups.
listingsRedis sorted setslisting:{sub}:{sort}[:{t}] → ZSET(post_id, score)capped at 1000Precomputed per subreddit and sort. Front page = merge of subscribed subreddits' hot lists.
subscriptionsPostgres(user_id, subreddit_id) PKjoined_atUsed to build the home feed; cached per user.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Upvote a post
- 1Client sends POST /vote {item: t3_abc, dir: +1}; UI shows the new state immediately (optimistic)
- 2Vote Service upserts (user, item) → +1 in the votes table and reads the previous direction to compute the delta (e.g. from -1 to +1 = +2)
- 3If the delta is non-zero it publishes VoteChanged(item, delta, ts) to Kafka, partitioned by item_id
- 4Anti-abuse checks run (rate per user, account age, IP/device clusters); suspicious votes are stored but weighted 0
- 5Score Aggregator consumes events, sums deltas per item over ~1-5 s windows and writes one UPDATE per item instead of one per vote
- 6It recomputes the hot score and updates the post's position in listing:{sub}:hot and other sorted sets in Redis
Open a thread with 30k comments
- 1Comment Service reads the thread from the post's partition (or a cached, pre-built tree for hot threads)
- 2It builds the tree in memory from parent_id links and sorts siblings by the chosen sort (best = Wilson lower bound)
- 3It returns the first ~200 comments by walking the tree breadth-limited (top-level first, up to depth ~8)
- 4Hidden branches are replaced by 'load more (n replies)' stubs holding child IDs
- 5The user's own votes for the visible comments are fetched in one batched lookup and merged
- 6For very hot threads, the sorted tree is cached for a few seconds and invalidated by new comments
7 Deep dives
Ranking: hot, top, best and controversial
Reddit's open-sourced 'hot' formula is roughly: score = log10(max(|ups - downs|, 1)) × sign + (created_at_seconds - epoch) / 45000. The log means the first 10 votes count as much as the next 90. The time term adds about 1 point every 12.5 hours, so a new post needs ~10x more net votes to beat one 12.5 hours older. A useful property: the hot score of a post only changes when it gets votes, not as time passes, because age is built into the score as a constant. So sorted sets in Redis stay valid without periodic re-scoring of every post. 'Top' is just net score within a time window (hour, day, week…), kept as separate sorted sets that are trimmed by time. For comments, 'best' uses the lower bound of the Wilson score confidence interval of the upvote ratio: a comment with 5 up and 0 down ranks below one with 100 up and 5 down, because we are less sure about the first. 'Controversial' favours many votes with a near-even split. Mentioning the Wilson score and the time-constant property of hot is a strong signal.
Votes without hot rows
A viral post can get thousands of votes per second. If every vote does UPDATE posts SET ups = ups + 1 WHERE id = X, all those transactions lock the same row and the database stalls. Split the problem. First, the source of truth for 'who voted what' is the votes table keyed by (user_id, item_id); an upsert makes votes idempotent (double-tap sends +1 twice, the second changes nothing) and lets us compute the delta when someone flips their vote. Second, scores are derived: vote deltas go to Kafka partitioned by item_id, and an aggregator sums them per item over short windows and applies one update per item per window. Thousands of votes become one write per second. Because the aggregate can drift if events are lost, a nightly job recomputes scores from the votes table and fixes differences. The UI shows the user's own vote immediately from the client, while public counts are eventually consistent. Reddit also fuzzes displayed counts slightly, which makes it harder for spammers to see whether their bot votes counted.
Storing and loading comment trees
Comments form a tree of unknown depth. Options: adjacency list (parent_id only) is simple to write but loading a tree needs recursive queries; nested sets make reads fast but inserts rewrite many rows; materialised path (store '0001.0042.0007') supports subtree queries with a prefix match and ordering by path. A practical design: store each comment with post_id, parent_id and depth, partitioned by post_id so the whole thread is one partition read, and assemble the tree in the Comment Service. A thread with 30k comments is ~12 MB of rows, too much to send to a phone, so the service sorts siblings and returns only the top part: top-level comments in sort order with a few levels of replies, cutting off branches with 'load more' stubs. For very hot threads (match threads, AMAs), keep the pre-sorted tree in cache and update it incrementally on new comments and score changes, refreshing every few seconds instead of per request. comment_count on the post is a denormalised counter updated asynchronously.
Listings and the home feed
Each subreddit has precomputed sorted sets in Redis for hot, new, rising and top-by-window, each capped at ~1,000 posts (nobody pages beyond that). The Score Aggregator updates positions as votes arrive; new posts are added to 'new' and 'hot' on submit. A subreddit page is: ZREVRANGE on the right set, then hydrate posts from Memcached, then filter removed/NSFW per user settings. The home feed for a logged-in user merges the hot lists of their subscribed subreddits (most users follow < 100). A k-way merge of the top ~50 from each list by hot score is cheap, and the result is cached per user for a minute or two. Because hot is comparable across subreddits, the merge is fair-ish, but large subreddits dominate; a per-subreddit normalisation or cap keeps small communities visible. Logged-out traffic (a large share from search engines) gets fully rendered pages cached at the CDN for 30-60 s, which removes most load.
8 Trade-offs
Sync vs async score updates
Updating scores synchronously keeps counts exact but creates hot-row contention and slows votes. Async aggregation via Kafka gives a few seconds of lag and needs reconciliation, but scales to viral posts. Users tolerate lag on public counts as long as their own vote shows instantly.
Precomputed listings vs query on read
SQL ORDER BY hot_score with an index works for small scale, but every vote moves rows in the index and hot pages repeat the same query thousands of times. Redis sorted sets are fast and cheap to update, at the cost of a cache rebuild path if Redis is lost.
Comment storage model
Adjacency list is simplest and fine when whole threads are loaded and assembled in memory. Materialised paths help subtree queries ('load more' for a deep branch). Nested sets are rarely worth it for write-heavy threads.
9 Bottlenecks
Viral post vote storm
One item's Kafka partition gets thousands of events/s. The aggregator batches per item, so writes stay at ~1/s per item. If one partition is overloaded, pre-aggregate in the Vote Service memory for 1 s before publishing.
Live threads
A cricket match thread gets hundreds of comments per minute and thousands of readers refreshing. Serve a cached tree refreshed every few seconds and default 'new' sort for live threads; push new comment counts over a WebSocket instead of full reloads.
10 Failure handling
Redis listings lost
Rebuild from Postgres: for each active subreddit, select recent posts and compute hot scores; serve stale CDN pages meanwhile. Rebuild most-active subreddits first.
Aggregator lag or crash
Votes are already stored, so nothing is lost. Scores lag; when the consumer restarts it resumes from committed offsets. Aggregated updates must be idempotent per window, or use the nightly recompute to correct double-applied windows.
Vote manipulation campaign
Abuse detection marks clustered votes (same IP ranges, new accounts, timing patterns) and sets their weight to zero, then recompute affected scores from the votes table.
11 What impresses
- Explaining why the hot score does not need periodic recomputation
- Using the Wilson lower bound for comment 'best' sort
- Separating vote truth from derived counts, with a reconciliation job
- Assembling and truncating comment trees in the service for huge threads
- Caching rendered pages for logged-out traffic at the CDN
- Vote fuzzing and abuse weighting instead of blocking
12 Mistakes
- UPDATE posts SET score = score + 1 on every vote
- Computing hot ranking with a full table sort on each request
- Recursive SQL query per comment level for a 30k-comment thread
- Allowing duplicate votes because voting is not idempotent
- Ignoring vote manipulation
- Returning the entire comment tree to mobile clients
13 Follow-ups
Lessons this design uses
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.