Scope the requirements
Two data-shape problems dominate: comment threads (deep trees, load partial by depth) and per-subreddit feeds (ranked by upvote velocity). Everything else is web-scale plumbing.
Functional — what it must do
- Submit post to a subreddit; comment on posts (nested).
- Upvote/downvote posts and comments.
- Ranked feeds: hot, new, top; subreddit + user home.
- Search across posts + comments.
- Explicitly cut: chat, awards, moderation tools — offer them back.
Non-functional — the numbers that shape the design
- Feed latency p95 under 200 ms for the top of a subreddit.
- 500M MAU, ~500 posts/s and ~5k comments/s at peak, 100k votes/s.
- Comment tree: some threads have 100k+ comments 20 levels deep — must load incrementally.
- Hot ranking updates within a minute of new activity.
- 99.9% on writes; reads must degrade gracefully to slightly-stale.
Reddit is a database schema problem in disguise. The comment tree and the hot-rank rollup are the parts I'll draw first.
Back-of-envelope estimates
The scary number is comment tree fan-out on hot threads. Total scale is modest; hot-spot handling is the challenge.
| Quantity | Estimate | How you got there |
|---|---|---|
| Posts/day | ~40M | 500/s avg, spikier around news events. |
| Comments/day | ~400M | 10 comments per post average; some threads have 100k+. |
| Votes/day | ~10B | 100k/s peak. Most votes are upvotes on already-hot content. |
| Storage (5 yr) | ~150 TB | comments dominate. Compressed markdown + counters is ~500 B per comment. |
| Feed cache | ~5 GB per subreddit hot page | top 1000 posts × 2 KB serialised. Fits in Redis trivially. |
| Vote counters | 10B/day INCRs | batched to per-minute Redis buckets; written back to Postgres per hour. |
The ~400M comments a day is small. What blows up is one thread with 100k comments — that's the hot-shard problem.
Sketch the API
REST for posts + comments; paginated with cursors. Comment tree endpoint returns partial trees to avoid loading huge threads.
- Comment tree returns a slice, not the whole thing — the client decides how deep and how wide. Load-more requests are separate calls.
- Vote endpoint is fire-and-forget from the user's perspective; server ack means 'we've queued this'.
Data model
Posts and comments are the two big tables. Comments use a materialised path so tree queries are prefix scans, not recursive joins.
path = materialised ancestor chain e.g. 'p123.c1.c5.c9'. Enables prefix scans for subtree loads.Postgres for posts + comments (structured, transactional, and I need prefix indexes on path). Redis for vote counters + hot-rank feed cache. Cassandra for vote history when it grows past what Postgres can hold. Elasticsearch feeds search via CDC.
High-level design
Read + write paths look normal; the interesting parts are the comment tree service and the hot-rank recomputation loop.
- Post submit → post service inserts row → emits event; hot-rank job picks up new post at next tick and inserts into subreddit's Redis ZSET with initial score.
- Feed request → feed service does ZREVRANGE on the subreddit's ZSET → hydrates top N post IDs with metadata from Postgres → returns.
- Comment write → comment service computes
path = parent.path + '.' + new_id→ inserts. Tree loads by prefix scan onpath. - Vote → vote service checks Cassandra for (user, target) to prevent double-vote → INCR counter in Redis → optionally enqueue for Postgres write-back later.
- Hot-rank job runs every ~30 s per active subreddit: reads recent votes + post age from Redis, recomputes score = f(upvotes - downvotes, age), updates ZSET.
Deep dives — where the interview is won
Comment trees: materialised paths make subtree loads cheap
The naive schema stores parent_id on each comment. Loading a subtree requires a recursive CTE — Postgres can do this, but it's slow at depth and impossible to page. The pattern: store the ancestor chain as a text column path, e.g. root=p123, child=p123.c1, grandchild=p123.c1.c5. A subtree query becomes WHERE path LIKE 'p123.c1%' ORDER BY path LIMIT 200 — a straight prefix index scan.
The path grows unbounded on deep threads, but capped at ~20 levels in practice (moderation limits, UX limits). At 20 levels × ~10 chars per segment = ~200 bytes per comment. Trivial. Load-more for deeper subtrees does another prefix scan with a longer prefix, paginated naturally.
Hot-rank formula and the recompute loop
Hot rank is a decay function: score = log10(max(|upvotes - downvotes|, 1)) + sign(upvotes - downvotes) * (epoch_seconds / 45000). Newer posts float; older posts sink unless votes keep coming. The log dampens mega-hits so they don't pin forever.
The recompute pattern: don't recompute on every vote (100k/s would crush it). Instead, run per-subreddit tick jobs every ~30 s that read recent vote counters from Redis, apply the formula to affected posts, and update the ZSET. Cold subreddits get less frequent ticks; hot ones get more. Front-page (r/all) is a ZUNIONSTORE across the top of every popular subreddit's ZSET, cached for 30 s.
One hot thread, 100k comments: shard the tree, not the post
A viral post's comment count can grow past what one Postgres shard row-set is comfortable serving concurrently. The fix isn't to shard the post — that would break the path prefix index. Instead: partition the comments table by post_id hash, so a single mega-thread's comments live on one shard but different threads distribute. The mega-thread shard gets more replicas + more RAM.
For the truly extreme case (r/AMA with a celebrity), route the thread's comments to a dedicated shard proactively. The router (a small config service) maps post_id → shard_id and updates as needed. Client-facing endpoints stay identical; the tree service handles the routing.
Follow-ups you should expect
How does vote fraud detection fit?
Front page (r/all) how does it stay fresh?
Comment sort orders — best, controversial, new?
sort:best, sort:new, sort:controversial, each with a different score formula. Load-more paginates within one ZSET. Cost is a few extra sorted sets per hot post — cheap in Redis.How do you handle a user deleting a comment mid-thread?
Search across all comments — how fresh?
Where candidates lose the room
- Loading full comment trees with a recursive CTE — dies on any large thread, and there's no cheap pagination.
- Recomputing hot rank on every vote — 100k writes/s to a sorted set is fine for Redis but rewriting rank per vote hides bugs.
- Sharding posts across threads for one mega-post — the tree lives together, always.
- Storing votes without dedup — a bug or refresh double-counts; the (user_id, target_id) primary key is the guard.
- Trying to serve r/all off the raw feeds — always cache the union with a short TTL.