Design Reddit
Design a discussion site organised into communities. Anyone can create a community; members submit link, text or image posts to it; other users vote posts and comments up or down and reply in deeply nested comment threads. Each community page and the personalised front page can be sorted by hot, new and top (today, this week, all time).
There are tens of millions of daily users, and most of them only read. A breaking news thread can collect 50,000 comments and hundreds of votes per second while also being the most viewed page on the site.
In 45 minutes, cover the data model for communities, posts and comment trees, how votes are recorded and counted, and how ranked listings are computed and served quickly.
Sketch on the HLD board, write your answer in the Design doc, and size it with the Estimator. Start the mock interview from the timer for a 45-minute run.
Design Path · Social & Messaging · problem 11 of 70
Ask before you design. Try answering each yourself, then compare.
- What scale should we plan for?
Typical answer
About 75 million daily users, 1 million posts and 10 million comments a day, and around 200 million votes a day.
- How fresh must rankings be?
Typical answer
Hot listings can lag votes by up to a minute. New posts must appear in the 'new' sort within a few seconds.
- Do vote counts need to be exact?
Typical answer
The stored total must be correct eventually, and a user can only vote once per item. Displayed counts may be slightly stale.
- How deep can comment threads go?
Typical answer
Arbitrarily deep in the data, but the page shows about 10 levels and the top few hundred comments, with 'load more' links.
- What does the front page contain?
Typical answer
For logged-in users, a merge of hot posts from communities they subscribe to (up to a few hundred). Logged-out users get a popular page shared by everyone.
- Is search or moderation tooling in scope?
Typical answer
Search is out of scope. Moderators removing posts and comments is in scope at a basic level.
Requirements
Functional
- Create communities; subscribe and unsubscribe.
- Submit posts; comment on posts and reply to comments to any depth.
- Upvote, downvote or clear a vote on any post or comment, once per user.
- List posts per community and on a personalised front page, sorted by hot, new or top over a time window.
- Show a post's comment tree sorted by best, top or new; moderators can remove content.
Non-functional
- Listing and thread pages p99 under 300 ms; 99.95% availability for reads.
- Votes accepted with p99 under 100 ms and reflected in rankings within 60 s.
- Handle ~20 K page views/s average and 60 K at peak, with single threads receiving hundreds of writes per second.
- Data is durable; vote totals are eventually exact.
Out of scope
- Full-text search, chat and ads.
- Spam and vote-manipulation models (mention the hooks).
- Media hosting (assume blob storage plus CDN).
Back-of-the-envelope estimates
- Reads: 75 M users × 20 page views ≈ 1.5 B/day ≈ 17,000/s, about 50,000/s at peak. Each listing page hydrates 25 posts, each thread page a few hundred comments.
- Writes: 200 M votes + 10 M comments + 1 M posts ≈ 211 M/day ≈ 2,400/s, peaking near 7,500/s. Votes are 95% of writes.
- Storage: comments 10 M × 1 KB ≈ 10 GB/day, posts 1 M × 2 KB ≈ 2 GB/day, vote records 200 M × ~30 bytes ≈ 6 GB/day. About 18 GB/day, 33 TB over 5 years and ~100 TB replicated: modest.
- Listing cache: for 100,000 active communities × 4 sorts × 1,000 ids × 16 bytes ≈ 6.4 GB, which fits comfortably in memory.
Storage is easy; the design is driven by read traffic on ranked listings and by hot threads that are being read and written heavily at the same time.
API
POST /v1/communities/{name}/posts {title, body|url|mediaId} -> 201 {postId}
GET /v1/communities/{name}/posts?sort=hot|new|top&t=day&after= -> {posts[], after}
GET /v1/feed?sort=hot&after= -> {posts[], after}
POST /v1/posts/{postId}/comments {parentId?, body} -> 201 {commentId}
GET /v1/posts/{postId}/comments?sort=best&depth=10&limit=200 -> {tree, moreIds[]}
PUT /v1/votes/{thingId} {dir: 1|0|-1} -> 204Voting is a PUT of the desired final state rather than an 'increment' call, so retries and double taps are harmless and changing a vote from up to down is one request.
Data model
communities(community_id, name UNIQUE, rules, created_at) and subscriptions(user_id, community_id) in a relational store; small and strongly consistent.
posts(post_id, community_id, author_id, title, body, created_at, score, ups, downs, comment_count, status) in a relational store sharded by community_id, so a community's 'new' listing is one shard and one index scan.
comments(post_id, comment_id, parent_id, path, author_id, body, score, created_at, status) in a wide-column store partitioned by post_id; the whole thread is one partition and path (for example 0007.0012.0003) preserves the tree for range reads.
votes(user_id, thing_id, dir, ts) in a key-value store keyed by (thing_id, user_id), the source of truth for 'one vote per user'. Listing cache: Redis sorted sets such as hot:{community} and top:day:{community} holding the top 1,000 post ids by score.
High-level design
Logged-out traffic, which is a large share of reads, is served from CDN-cached listing and thread pages with a TTL of about 30 seconds. Everything else goes through the API Gateway to four services.
Reading a listing. The Listing Service reads the community's sorted set from the Listing Cache (or, for the front page, merges the hot sets of the user's subscribed communities), takes the next 25 ids after the cursor, and hydrates them from the Post DB via a post cache. The 'new' sort comes straight from the Post DB's index on (community_id, created_at).
Reading a thread. The Comment Service reads the post's comment partition, builds the tree in memory, sorts each level, and trims to the requested depth and size, returning ids for 'load more'. Hot threads are cached as built trees for a few seconds.
Voting. The Vote Service upserts (thing, user) → dir in the Vote Store, computes the delta from the previous value, and publishes it to the Vote Events stream. Score Workers consume the stream, aggregate deltas per item for a few seconds, update counts in the Post DB and Comment Store, recompute hot and top scores, and update the sorted sets in the Listing Cache. Creating a post publishes an event too, so new posts enter the hot listing with a starting score.
Open the HLD board and press Reference to see the architecture diagram.
Deep dives
Hot and top rankings
A good hot score makes votes matter logarithmically (the first 10 votes count as much as the next 90) and gives newer posts a bonus that grows linearly with submission time. Because the bonus depends on creation time rather than current age, the score only changes when votes change, so it can be stored and kept in a sorted set instead of being recomputed on each read. Top is simply net score filtered by time window; keep one sorted set per window, and drop ids that age out with a periodic sweep. Listings keep only the top 1,000 ids, since almost nobody pages deeper. The personalised front page merges the heads of up to a few hundred community sets, which is a cheap k-way merge, and the result is cached per user for a minute.
Storing and serving comment trees
Store comments flat, one partition per post, each with parent_id and a materialised path. Fetching a thread is a single partition read, after which the service assembles the tree in memory and sorts siblings by a 'best' score: the lower bound of a confidence interval on the upvote ratio, so a comment with 5 up and 0 down does not outrank one with 900 up and 100 down. For a 50,000-comment thread, cap what is returned by depth and count and send ids for 'continue this thread'. Cache the built tree for a few seconds; with hundreds of readers per second, a slightly stale tree is far cheaper than rebuilding it per request.
Votes: records and counters
The vote record and the vote count have different needs. The record must be exact and idempotent, so it is an upsert keyed by (thing, user) and the service emits the difference between old and new direction (for example −1 to +1 is +2). Counters can lag, so deltas flow through a stream partitioned by thing id, which keeps each item's updates on one worker and lets that worker sum hundreds of votes into a single database write. This turns a viral post's 500 votes per second into a handful of writes. Old posts can be archived and their votes frozen, which keeps the hot working set small. Vote-manipulation detection consumes the same stream and can quietly discount suspicious votes.
Failure modes
- If the Listing Cache is lost, listings are rebuilt from the Post DB per community on first access (hot sets from the last few days of posts) while serving the 'new' sort meanwhile.
- If Score Workers fall behind, votes are still recorded and counts catch up; rankings are briefly stale but correct afterwards.
- If a Post DB shard holding a giant community is overloaded, the post cache and CDN absorb reads and that community can be moved to a dedicated shard.
- If the Vote Store is unavailable, voting fails fast with a retry prompt rather than silently counting votes that cannot be deduplicated.
Core algorithm in code
import math
EPOCH = 1_600_000_000
HALF_LIFE = 12 * 3600 # every 12 h of age is worth one factor of 10 in votes
def hot(ups, downs, created_ts):
"""Hot score: grows with the log of net votes, and newer posts get a steady bonus."""
net = ups - downs
sign = 1 if net > 0 else -1 if net < 0 else 0
return sign * math.log10(max(abs(net), 1)) + (created_ts - EPOCH) / HALF_LIFE
def confidence(ups, downs, z=1.96):
"""Lower bound of the upvote ratio, so 5/0 does not beat 900/100."""
n = ups + downs
if n == 0:
return 0.0
p = ups / n
centre = p + z * z / (2 * n)
spread = z * math.sqrt(p * (1 - p) / n + z * z / (4 * n * n))
return (centre - spread) / (1 + z * z / n)
def build_tree(comments):
"""comments: [{"id", "parent", "ups", "downs"}]. Return nested dicts,
children sorted by confidence, best first."""
nodes = {c["id"]: dict(c, children=[]) for c in comments}
roots = []
for c in nodes.values():
(nodes[c["parent"]]["children"] if c["parent"] else roots).append(c)
def order(lst):
lst.sort(key=lambda c: confidence(c["ups"], c["downs"]), reverse=True)
for c in lst:
order(c["children"])
order(roots)
return roots
def show(nodes, depth=0):
for c in nodes:
print(" " * depth + f"{c['id']} (+{c['ups']}/-{c['downs']}, conf={confidence(c['ups'], c['downs']):.2f})")
show(c["children"], depth + 1)
now = EPOCH + 400 * 86400
posts = {"old classic": (52_000, 3_000, now - 30 * 3600), "rising": (800, 40, now - 2 * 3600),
"brand new": (12, 1, now - 600), "controversial": (4_000, 3_900, now - 3 * 3600)}
print("hot order:")
for name, (u, d, ts) in sorted(posts.items(), key=lambda kv: hot(*kv[1]), reverse=True):
print(f" {name:<14} hot={hot(u, d, ts):.2f}")
print("comment tree:")
show(build_tree([
{"id": "c1", "parent": None, "ups": 5, "downs": 0},
{"id": "c2", "parent": None, "ups": 900, "downs": 100},
{"id": "c3", "parent": "c2", "ups": 40, "downs": 2},
{"id": "c4", "parent": "c2", "ups": 3, "downs": 9},
{"id": "c5", "parent": "c3", "ups": 7, "downs": 0},
]))
Trade-offs
- Precomputed sorted sets make listing reads cheap but cost write work on every vote batch and allow up to a minute of staleness.
- Storing comments flat in one partition per post makes thread reads one query but creates very large partitions for megathreads.
- CDN caching for logged-out users removes most read load but costs freshness and makes per-user state (your own votes) a separate call.
- Aggregating vote deltas in workers protects the database but means displayed counts lag the true total by a few seconds.
- Capping listings at 1,000 ids saves memory but makes very deep paging fall back to slower database queries.
What a strong answer shows
- Explains a hot score that combines log-scaled votes with recency, and why it can be stored rather than recomputed.
- Precomputes listings per community and sort, and builds the front page by merging them.
- Stores comment trees so a thread is one read, and sorts siblings by a confidence-based score.
- Separates idempotent vote records from asynchronously aggregated counters.
- Identifies hot threads and hot posts and uses caching and batching to protect storage.
- Notes that most traffic is reads and uses the CDN for logged-out users.
Mistakes to avoid
- Computing hot rankings with ORDER BY over all posts on every request.
- Fetching comments recursively, one query per level or per comment.
- Incrementing a counter on the post row synchronously for each vote, creating hot rows.
- Letting a user vote multiple times by not keying votes on (user, thing).
- Sorting comments by raw net score, which buries good new comments under early ones forever.
Follow-ups to expect
- How would you implement 'rising'? (Rate of votes over the last hour relative to age; another sorted set fed by the same stream.)
- How would you handle a moderator removing a post? (Status flag, remove from listing sets, CDN purge for the thread page.)
- How would you shard a community that is far larger than the others? (Dedicated shard, and split comments by post which already distributes load.)
- How would you stop vote brigading? (Detect correlated voting from linked accounts in the stream and discount those votes.)