The Blueprint
Social & feeds intermediate

Design Reddit

Nested comment trees, hot-ranking algorithms and per-subreddit feeds — a social product where structure of data is more interesting than volume.

asked at Reddit · Discord · Meta Sometimes asked ~40 min walkthrough ✎ Practice on canvas →
01

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.
Say this out loud

Reddit is a database schema problem in disguise. The comment tree and the hot-rank rollup are the parts I'll draw first.

02

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.

QuantityEstimateHow you got there
Posts/day~40M500/s avg, spikier around news events.
Comments/day~400M10 comments per post average; some threads have 100k+.
Votes/day~10B100k/s peak. Most votes are upvotes on already-hot content.
Storage (5 yr)~150 TBcomments dominate. Compressed markdown + counters is ~500 B per comment.
Feed cache~5 GB per subreddit hot pagetop 1000 posts × 2 KB serialised. Fits in Redis trivially.
Vote counters10B/day INCRsbatched to per-minute Redis buckets; written back to Postgres per hour.
Say this out loud

The ~400M comments a day is small. What blows up is one thread with 100k comments — that's the hot-shard problem.

03

Sketch the API

REST for posts + comments; paginated with cursors. Comment tree endpoint returns partial trees to avoid loading huge threads.

GET/api/v1/r/{sub}?sort=hot&after=→ paginated feed. Reads from precomputed sorted set.
POST/api/v1/r/{sub}/postsbody: { title, body, kind }. Publishes + kicks fan-out.
GET/api/v1/posts/{id}/comments?depth=5&limit=200→ tree slice: root + children up to depth. Load-more for deeper subtrees.
POST/api/v1/commentsbody: { postId, parentId?, body }. Updates materialised path.
POST/api/v1/votesbody: { targetId, targetType, dir }. Batched in Redis, aggregated to counters + hot-rank.
  • 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'.
04

Data model

Posts and comments are the two big tables. Comments use a materialised path so tree queries are prefix scans, not recursive joins.

posts
id PK · subreddit_id · author_id · title · body · created_at · score · rank_hot
Postgres, sharded by subreddit_id. Small per-post size.
comments
id PK · post_id · parent_id? · author_id · body · created_at · path · score
path = materialised ancestor chain e.g. 'p123.c1.c5.c9'. Enables prefix scans for subtree loads.
votes
(user_id, target_id) PK · dir · ts
prevents double-vote per user. Cassandra scale; hot writes to Redis first.
feed_cache
subreddit_id → sorted set of (post_id, rank_hot)
Redis ZSET, updated by hot-rank job. Feed reads are ZREVRANGE.
The database call

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.

05

High-level design

Read + write paths look normal; the interesting parts are the comment tree service and the hot-rank recomputation loop.

client edge service data async Web/app API gateway Feed svc Post svc Comment svc path index Vote svc Redis ZSET hot feed Postgres posts, comments Cassandra votes Hot-rank job CDC Search index rerank path prefix counter
vote → redis counter → hot-rank job → sorted set → feed reads. tree reads hit postgres path index.
  1. 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.
  2. Feed request → feed service does ZREVRANGE on the subreddit's ZSET → hydrates top N post IDs with metadata from Postgres → returns.
  3. Comment write → comment service computes path = parent.path + '.' + new_id → inserts. Tree loads by prefix scan on path.
  4. Vote → vote service checks Cassandra for (user, target) to prevent double-vote → INCR counter in Redis → optionally enqueue for Postgres write-back later.
  5. 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.
06

Deep dives — where the interview is won

Phase 01

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.

query path index GET subtree c1 path LIKE 'p1.c1%' index range scan prefix
one prefix predicate returns the entire subtree in one range scan — no recursion.
Trade-offThe path breaks if you move a comment (rare on Reddit, but common on forums). A move requires rewriting all descendants' paths — an operation you accept as expensive because it happens rarely.
Phase 02

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.

signals rerank feed Vote counters Age tick Hot-rank job per subreddit 30s Sorted set ZADD score update scores
signals feed a periodic job. no vote directly rewrites the sorted set — reduces write amplification.
Trade-offRank is up to 30 s stale. For a comment sorting feature this is invisible; for a betting site this would be intolerable. Reddit's UX tolerates it comfortably.
Phase 03

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.

requests tree svc shards GET tree Comment svc post→shard Shard A Hot shard +replicas normal hot post
hot posts move to dedicated shards with more replicas. path index stays intact.
Trade-offMigrating a hot post's comments to a dedicated shard mid-life is complex — dual-write during migration, cutover atomically. Usually done pre-emptively when a mod signals a big AMA is coming.
07

Follow-ups you should expect

How does vote fraud detection fit?
Every vote lands in Cassandra with user_id + target_id + ts + ip. Nightly batch runs anomaly detection (many votes on one target from same IP block, non-human vote patterns, brigade coordination). Suspected fraud downweights those votes in the hot-rank recompute; obvious brigades zero them out. Cheap because the batch reads the same data used for royalty-style analytics.
Front page (r/all) how does it stay fresh?
Union of top-K from every popular subreddit's ZSET into a global ZSET, cached for 30 s. Recomputed by a global tick job. Cold subreddits are excluded to keep the union bounded.
Comment sort orders — best, controversial, new?
Store multiple ZSETs per post: 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?
Tombstone: mark row deleted but keep the path so children still hierarchically connect. UI shows '[deleted]'. Prunes on old threads happen in batch to reclaim storage; keeping the path avoids re-parenting all descendants.
Search across all comments — how fresh?
CDC from Postgres → Kafka → Elasticsearch. New comments appear in search within ~10 s. Full re-index of a subreddit runs weekly to catch bugs. The search index is the second-largest storage line item after the comment table itself.

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.

Concepts this leans on

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.