SysDesignPrep.com
System design interview question

Design Reddit

Serve communities of posts with deeply nested comment threads, take tens of thousands of votes a second, rank by hot and best, and keep a 50,000-comment thread fast while it is still growing.

Last updated 2026-09-30. Difficulty: medium. Patterns: comment-trees, voting, ranking, listings, caching. Reported at Reddit, Meta, Amazon, Google, Discord.

Walk through a strong candidate's answer, turn by turn.

The interviewer asks, the candidate answers and draws, and you press Next. Pause to answer yourself at the key decisions, and ask the AI Mentor anything along the way.

Functional requirements

  • Communities and posts. Users subscribe to subreddits and submit link, text or image posts to them.
  • Nested comments. Comments reply to the post or to other comments, to any depth. Threads can grow to tens of thousands of comments.
  • Votes. One up or down vote per user per post or comment, changeable. Scores are shown everywhere.
  • Ranking. Posts by hot, new, top and rising; comments by best, top, new and controversial.
  • Home and subreddit listings. A subreddit’s front page, and a home feed merging the user’s subscriptions.
  • Out of scope. Chat, ads, search, media hosting beyond a CDN, and moderation tooling beyond removal.

Non-functional requirements

  • Read-heavy (~26 k thread views/s). Hundreds of reads per write. Most traffic is people reading popular threads, much of it logged out.
  • Thread latency (p99 < 500 ms for the first page of a thread). This is the requirement that drives the hardest trade-off: a thread is a tree of thousands of comments sorted by a score that changes with every vote, and it must render fast while it is still growing.
  • Vote volume (~17 k votes/s avg, hot threads 10 k/s each). Votes are the most frequent write, and they concentrate on whatever is popular right now.
  • Freshness (new comments visible in seconds; scores within seconds to a minute). People reply to each other in real time; exact scores can lag.
  • Correctness of votes. One vote per user per item, idempotent under retries, and resistant to manipulation.
  • Availability (99.9 %). Degrade by serving cached threads and listings when writes or ranking lag.

Back-of-envelope estimates

  • Thread views per second: ~26 k. 75 M DAU × 30 thread views a day = 2.25 B/day ÷ 86 400 ≈ 26 k/s. Logged-out views are a large share and can be served from the CDN.
  • Votes per second: ~17 k. 75 M × 20 votes a day = 1.5 B/day ÷ 86 400 ≈ 17 k/s. A front-page thread can take 10 k/s on its own.
  • Comments per second: ~65. 5.5 M comments a day ÷ 86 400 ≈ 64/s. Writes are tiny next to reads; the cost is keeping trees and caches current.
  • A big thread: ~50 k comments · ~25 MB. 50 k comments × ~500 B each ≈ 25 MB. Far too much to send to a browser; the first page shows ~200 comments and "load more" links.
  • Vote records per year: ~550 B. 1.5 B votes a day × 365 ≈ 550 B (user, item, direction) records a year, at ~20 B each ≈ 11 TB. Stored so a user sees their own votes and cannot vote twice.
  • Subreddit listings: ~100 k active. About 100 k subreddits with recent activity, each with hot, new, top and rising listings of the top ~1,000 post ids. Small enough to precompute and keep in memory.

Components

  • Browser / app: Reads listings and threads, votes, and comments. Logged-out users get cached pages; logged-in users get personal overlays (their own votes, collapsed comments).
  • CDN (logged-out pages): Caches whole pages and API responses for logged-out users for tens of seconds. A large fraction of all reads never reaches the origin.
  • API gateway: Authentication, rate limits on votes and comments (spam and brigading), and routing.
  • Listing service: Serves subreddit front pages from precomputed listings and builds the home feed by merging the listings of the user’s subscribed subreddits.
  • Listings (per subreddit × sort): Top ~1,000 post ids per subreddit for hot, new, top and rising, kept in memory and updated by the ranking jobs.
  • Ranking jobs (hot · best): Consume vote and comment events, recompute hot scores for posts and best scores for comments, and update listings and thread sort orders.
  • Comment service: Writes comments with their place in the tree and builds the rendered view of a thread: sorted, truncated by depth and count, with "load more" markers.
  • Comment store (partitioned by post · path ordered): All comments of a post in one partition, each with its parent and a materialised path, so a whole thread (or a subtree) is one range read.
  • Thread cache (post × sort → rendered tree): The first page of each popular thread per sort order, ready to serve. Patched on new comments, rebuilt on a short timer as scores change.
  • Vote service: Records one vote per (user, item), handles changes and retractions, and emits score deltas. Never updates a score synchronously on the hot item itself.
  • Vote store (key = (user, item)): Every user’s vote on every item, for "did I vote?" overlays and to make voting idempotent. Partitioned by user, with a per-item index for recounts.
  • Score counters (sharded per hot item): Up and down counts per item, updated from vote deltas and split across sub-counters for hot items. Snapshotted to the item rows periodically.
  • Post service: Submits and serves posts; media is uploaded to object storage behind the CDN.
  • Post store: Posts with subreddit, author, title, body or link, score snapshot and comment count. Sharded by post id.
  • Events (Kafka): Votes, comments and posts as events, driving ranking, counters, notifications and anti-manipulation detection.

User flows

  1. Open a popular thread. The main read path: logged-out readers hit the CDN, logged-in readers hit a cached rendered tree with their own votes overlaid; only a miss reads the comment store.
    1. A logged-out reader’s request is answered by the CDN. Threads are cached at the edge for 30 to 60 seconds for logged-out users, who see the same page. A popular thread’s logged-out traffic becomes one origin request a minute per edge.
    2. A logged-in reader’s request goes to the comment service, which reads the cached first page for this sort.
    3. On a miss, it reads the post’s comments in path order from one partition and builds the tree. One range read returns every comment with its parent and path. The service sorts each level by the requested order, keeps the top children per level and a maximum depth, and replaces the rest with "load more" stubs that carry counts.
    4. It overlays the reader’s own votes on the visible comments. The cached tree is shared; personal state is not. A batch lookup of the reader’s votes for the ~200 visible ids marks which arrows are highlighted.
    5. Expanding "load more" fetches one subtree by path prefix.
  2. Reply to a comment. A small write that must appear in seconds: insert with a path, patch the cached tree, and let counts and ranking catch up asynchronously.
    1. The user posts a reply to a comment.
    2. The comment is inserted with a path built from its parent’s path. Path = parent path + a sortable segment for the new id (0001.0007.0042), so the comment lands next to its siblings in path order and its subtree is a prefix range. Depth is the number of segments.
    3. The author sees the reply immediately; the cached tree is patched. Inserting the new comment under its parent in the cached tree is cheaper than a rebuild, and readers see it within seconds. The author’s own client inserts it locally at once.
    4. A comment-created event updates counts, ranking and notifications. The post’s comment count, the parent author’s inbox notification and the post’s hot score (comments are a signal) are all asynchronous.
  3. A thread hits the front page. The scale case: 10 k votes a second on one post, thousands of new comments, and millions of readers. Every write path is absorbed asynchronously; reads come from caches.
    1. Votes on the post and its top comments arrive at 10 k/s. Each vote is an upsert into the voter’s own partition, so writes spread across millions of users. Nothing touches the post’s row synchronously.
    2. Score deltas are aggregated into sharded counters. The counter for a hot item is split into many sub-counters; consumers batch deltas per item per second. The displayed score lags by a second or two, which nobody can tell.
    3. Comment sort orders are recomputed on a short timer, not on every vote. Re-sorting a 50 k-comment tree for every vote would be millions of sorts a second. Every few seconds, the ranker rebuilds the cached first page with fresh scores, and concurrent rebuild requests are coalesced into one.
    4. Readers are served from the CDN and the thread cache. The comment store sees the rebuilds and "load more" requests, not every view. If the store struggles, readers still get a page that is a few seconds old.
    5. Scores are snapshotted to the post row every few seconds. Listings and cold reads use the snapshot, so they never read the counter shards directly.
  4. Change a vote, twice, with a flaky connection. The correctness path: one vote per user per item, idempotent under retries, and counts that converge to the truth.
    1. The user upvotes, then switches to a downvote; the app retries both because of a bad connection.
    2. The vote store upserts the (user, item) row and returns the previous direction. The delta is computed from previous and new direction: up to down is −2, a repeat of down is 0. Applying the same request twice produces a zero delta the second time.
    3. The delta flows to the counters. Counters converge as long as every delta is applied exactly once; events carry the vote row’s version so a replayed event can be detected.
    4. A nightly job recounts recently active items from the vote store. The vote store is the source of truth. Recounting corrects any drift from lost or duplicated events, and removes votes from accounts later found to be manipulating.
  5. Hot listings and the home feed. The background path: ranking jobs keep each subreddit’s listings current, and the home feed is a merge of the subscribed listings at read time.
    1. Ranking jobs consume vote and comment events and recompute post hot scores. Hot combines the log of the net score with the post’s age, so a new post with a few votes can outrank an old post with many, and every post decays over time without being touched.
    2. Each subreddit’s hot, new, top and rising listings are updated in memory. Each listing is a sorted set of the top ~1,000 post ids. Only posts whose score changed are re-inserted, so the work follows activity.
    3. A user opens the home feed; the listing service merges their subscriptions’ listings. Fan-out on read: take the top of each subscribed subreddit’s hot listing (users subscribe to tens or hundreds), merge by score with a per-subreddit cap so one big community does not drown the rest, and paginate.
    4. Post details are hydrated from the post store. A batch read by post id, cached per post. Scores come from the snapshot, which is seconds old.

Deep dives

Storing a comment tree

Comments nest to any depth. How do you store them so a whole thread, or one subtree, loads in one query?

The natural model is an adjacency list: each comment stores its parent id. Building a thread then means following parent links level by level, which is either many queries (one per level) or a recursive query that the database executes with many lookups.

Reads outnumber writes by hundreds to one, and comments are almost never moved to a new parent. That makes it worth doing a little extra work on insert to make reads a single range scan.

  • Materialised path per comment, all comments of a post in one partition, ordered by path chosen
  • Adjacency list with recursive queries situational: small threads, or when the database handles recursive CTEs well and everything is cached
  • Closure table (every ancestor–descendant pair) situational: when subtrees are queried in many different ways
  • Nested sets (left/right numbers) rejected

The answer: Store comments in a table partitioned by post id with (post_id, path) as the clustering key. The path is the parent’s path plus a fixed-width, sortable segment for the new comment (base-36 of a per-post counter), so siblings sort by creation and every subtree is a contiguous prefix range. The comment service reads a thread with one range scan, builds the tree in memory, sorts each level by the requested order, and cuts it to a page with "load more" stubs. Parent id is stored as well for convenience. Reddit itself has long rendered threads by building trees in the application from comment lists, with heavy caching on top.

A thread has 50,000 comments. Do you really read them all on a cache miss?

For the first page of a sort like "best", yes, but rarely: the result is cached and rebuilt on a timer, so the full read happens a few times a minute for a hot thread. For "new" you can read the newest comments by time index instead. Storing a precomputed sort key per comment can also let the read stop early.

How do you show a deleted comment that has replies?

Keep the row with a "deleted" flag and blank body, so the path and the replies under it still render ("[deleted]"). Removing the row would orphan its subtree.

Why fixed-width path segments?

So paths sort correctly as strings: "0002" before "0010", whereas "2" sorts after "10". Fixed width (or a length prefix) keeps path order equal to tree order.

How deep can it go?

Storage allows any depth, but the renderer caps visible depth (say 8 to 10) and shows "continue this thread" beyond it, which opens the subtree as its own page. Very deep paths are just longer strings.

Counting votes

A front-page thread gets 10,000 votes a second. How do you record them and keep scores right?

Two facts must be kept: each user’s vote on each item (so they see their arrow highlighted and cannot vote twice) and each item’s total. Incrementing the item’s score row on every vote puts thousands of writes a second on one row, which serialises on its lock.

The individual vote is the source of truth; the score is derived. Scores can lag a second or two without anyone noticing, which allows the derived part to be asynchronous and aggregated.

  • Upsert per (user, item) as the truth; score deltas aggregated asynchronously into sharded counters; periodic recount chosen
  • Increment the item row on every vote rejected
  • Count votes only, no per-user records rejected

The answer: Votes are stored as one row per (user, item) with the direction, partitioned by user. A vote request states the desired direction, and the upsert returns the previous direction, so the score delta is computed exactly and retries produce zero deltas. Deltas go through Kafka to counter consumers that batch per item per second and apply to sharded counters for hot items. Scores are snapshotted onto post and comment rows every few seconds for listings and cold reads. A nightly recount from the vote store corrects drift and applies anti-manipulation decisions. Displayed scores on very popular items are lightly fuzzed, as Reddit does, which makes vote-bot feedback harder to read.

How do you detect vote manipulation?

From the event stream: clusters of accounts that vote on the same items within seconds, new accounts voting in bursts, votes from the same IP or device ranges. Suspect votes are kept but excluded from counts; the recount applies the exclusion, so manipulation stops paying off.

Why store votes by user rather than by item?

The common reads are "what did this user vote on these 200 visible items" (to highlight arrows), which is one partition read by user. Counting per item is done from the delta stream, and recounts use a secondary index or a batch scan by item.

Why does Reddit fuzz vote counts?

Bots and spammers test whether their votes count by watching the score. Adding small noise to displayed counts on busy items hides the effect of individual votes, while the ranking uses the true values.

A user deletes their account. What happens to their votes?

Their vote rows are deleted and a recount job adjusts the totals of the items they voted on, by emitting negative deltas or recounting those items. Because votes are the source of truth, this is mechanical rather than a manual correction.

Hot and best

How do "hot" for posts and "best" for comments work, and why not just sort by score?

Sorting posts by score shows yesterday’s winners forever and gives new posts no chance. Sorting comments by score rewards the earliest comments, which collected votes while everyone else was still typing, and treats 1 upvote out of 1 as better than 950 out of 1,000.

Posts need a score that combines popularity with recency. Comments need a score that reflects how good a comment is likely to be, accounting for how many people have actually voted on it.

  • Hot: log of net score plus age; Best: lower bound of the Wilson confidence interval on the upvote ratio chosen
  • Sort by net score situational: the "top" sort over a fixed time window
  • Learned ranking model situational: personalised home feeds, alongside the transparent sorts

The answer: For posts, hot = sign(s) × log10(max(|s|, 1)) + (created_seconds − epoch) / 45000, where s is the net score: each tenfold increase in votes is worth about 12.5 hours of recency, so newer posts rise and every post sinks over time on its own. Because the time term only grows, scores never need recomputing just because time passed. For comments, best = the lower bound of the Wilson score interval for the proportion of upvotes, which is low when there are few votes and approaches the true ratio as votes accumulate. Both are computed by ranking jobs from counter values and stored with the item, so sorting is cheap.

Why does hot never need a timer to decay posts?

Because instead of subtracting age from old posts, it adds creation time to new ones. Every new post starts higher than every old one by the time elapsed, so old posts fall relative to new ones without anyone touching them.

What does "controversial" mean computationally?

High total votes with a ratio near 50/50: for example (ups + downs) raised to the power of the balance, where balance is the smaller count divided by the larger. Items with many votes split evenly rank first.

How would you rank the home feed if subscriptions vary wildly in size?

Normalise per subreddit (cap posts per subreddit per page, or compare scores relative to the subreddit’s typical score) so a small community’s best post can appear next to a huge community’s. Otherwise the biggest subreddits fill every page.

Should hot use upvotes minus downvotes or the ratio?

Hot uses the net score, so heavily downvoted posts sink. Best uses the ratio with a confidence bound because comments compete within one thread where exposure differs; net score there would mostly reward being early.

Keeping hot threads fast while they change

Popular threads change every second with new comments and votes. How do you cache them without serving stale or broken trees?

The thread view is expensive to build and read thousands of times a second, so it must be cached. But it changes constantly: new comments every few seconds and scores changing with every vote. Invalidating on every change would mean rebuilding constantly; never invalidating means serving a frozen thread.

Different changes need different freshness. A new reply should appear within seconds, especially to its author. Comment order shifting by a place or two can wait a few seconds.

  • Cache rendered first pages per (post, sort); patch on new comments; rebuild on a short timer for score changes; coalesce rebuilds; CDN for logged-out chosen
  • Invalidate on every change rejected
  • Long fixed TTL only rejected

The answer: The comment service caches the rendered first page of each thread per sort order. A new comment is inserted into the cached tree under its parent (or its parent’s "load more" count is incremented) immediately. Score changes do not invalidate; instead hot threads are rebuilt every few seconds by the ranker with fresh scores, with a single-flight lock so concurrent misses trigger one rebuild. Personal state (own votes, collapsed comments) is overlaid per request and never cached in the shared tree. Logged-out pages are cached at the CDN for 30 to 60 seconds.

A moderator removes a comment. How fast does it disappear?

Removal is an explicit invalidation, not a timer: the comment is marked removed in the store and the cached trees and CDN entries for that thread are purged at once. Moderation actions are rare enough to invalidate eagerly.

What is single-flight and why does it matter here?

When the cache entry for a hot thread expires, thousands of requests miss at once. Single-flight lets the first build and makes the rest wait for its result (or serve the slightly stale copy), so the comment store sees one read instead of thousands.

How do you avoid sending the same 200 comments to someone who refreshes?

Use conditional requests: the cached page carries a version, and the client sends it back; if nothing changed, the server answers 304. For live threads, a lightweight 'new comments since' endpoint lets the client fetch only additions.

How big is the thread cache?

Only threads read recently are cached, at a few hundred KB per rendered page per sort. Tens of thousands of active threads are a few gigabytes, which fits a modest cache cluster; cold threads are rebuilt on demand.

Front pages and the home feed

How do you build a user’s home feed from the hundreds of subreddits they follow?

A home feed merges posts from every subscribed subreddit, ranked by hot. Pushing every post into every subscriber’s feed (fan-out on write) would mean millions of writes for a post in a large subreddit, which can have tens of millions of members.

But the number of subreddits is small compared with the number of users, and each subreddit’s own listing is shared by all its readers. Precomputing per subreddit and merging per user at read time is cheap.

  • Precomputed per-subreddit listings; merge subscriptions at read time with per-subreddit caps chosen
  • Fan-out on write to per-user feeds rejected
  • Query posts across subscribed subreddits on every request rejected

The answer: Ranking jobs maintain, for each active subreddit, sorted sets of the top ~1,000 post ids for hot, new, top and rising, updated as votes arrive. A subreddit page reads its listing directly. The home feed reads the top of each subscribed subreddit’s hot listing (with a cap for users subscribed to hundreds), merges by score with a per-subreddit limit per page, and caches the merged result per user for a minute for pagination. Hydration reads post rows by id from a post cache. This is fan-out on read, the opposite choice from Design a News Feed, because here the number of sources is small and their audiences are huge.

A user is subscribed to 800 subreddits. Does every home page read 800 listings?

No: cap the merge at the most active few hundred subscriptions, weighted by how often the user engages with each, and cache the merged feed for pagination. Rarely visited subscriptions appear occasionally rather than every time.

Why is fan-out on read right here when news feeds use fan-out on write?

A news feed has as many sources as users, each with a small audience, so pushing is cheap per post and pulling would mean merging hundreds of user timelines. Reddit has relatively few sources with enormous audiences, which flips the cost: pulling from shared per-subreddit listings is cheap, pushing to millions of feeds is not.

How does 'rising' work?

It favours posts gaining votes quickly relative to their age, computed from the recent vote rate rather than the total. It needs the delta stream, which the ranking jobs already consume, so it is a different score over the same events.

What happens to listings when a post is removed?

The removal event deletes the post id from every listing it is in, and the merged home feeds that are cached for a minute expire quickly. Hydration also skips removed posts, so a stale id never renders.

The theory behind it

  • Data modelling for reads: Modelling from access patterns rather than entities: denormalisation, precomputed views, fan-out on write versus read, and the write amplification each choice buys you.
  • Caching: Where to cache (browser, CDN, application, database), cache-aside vs write-through vs write-back, eviction policies, invalidation, hot keys, thundering herds and cache stampedes.
  • Batch and stream processing: Stream versus batch, windowing and watermarks, late and out-of-order data, exactly-once counting, approximate algorithms, and the lambda/kappa argument in one paragraph.

Related

Open in your browser to sign in

Google does not allow sign-in inside this app's built-in browser. Open this page in Safari and sign in there. The link opens this same page.

Tap the ⋯ or share button at the top or bottom of the screen, then Open in browser. Or copy the link and paste it into Safari.