Knowledge Guide
HomeSystem DesignSystem Design Problems

medium Designing Reddit

The two correctness-critical pieces

A Reddit-style backend is read-heavy and rank-driven, so two decisions carry most of the design's weight and are the easiest to get subtly wrong: how a post's ranking score is computed, and how a post reaches every subscriber's feed. This rewrite traces both end-to-end with worked numbers, and fixes a bug in the ranking formula carried by the original page.

The original stated Reddit's "hot" score as log10(|ups - downs|) + sign(v) · (post_age_in_seconds / 45000). That is wrong in two ways. First, it adds a positive multiple of post_age, so an older post scores higher — the opposite of keeping a feed fresh. Second, post_age = now - created is measured from the current clock, so every post's score changes on every read and the ordering is never stable. Reddit's real algorithm instead uses the post's absolute creation time as the time term, computed once at submission. The corrected formula and a full trace are in the ranking section below.

diagram
diagram

Write flow, traced: creating a post

User A submits a link post to r/systems. The write path never blocks on fan-out:

  1. Gateway. POST /posts hits the API gateway, which authenticates A's token and routes to the Post Service.
  2. Persist. The Post Service mints a globally unique post_id (Snowflake), then writes one row to the sharded posts store — partitioned so a subreddit's posts co-locate, e.g. primary key (subreddit_id, post_id) in Cassandra. Denormalized fields (score, comment_count, author username) are stored inline to avoid cross-shard joins on read.
  3. Initial rank. Because the time term depends only on created_utc (see below), the Post Service computes the post's hot score right here, once, and stores it. It will never need recomputing as the clock advances.
  4. Enqueue fan-out. The service does not synchronously write into subscribers' feeds. It publishes a PostCreated{post_id, subreddit_id, hot} job to a fan-out queue and returns 201 Created to A in a few milliseconds.
  5. Async materialize. Feed workers consume the job and, for push-eligible subreddits, insert (post_id, hot) into each subscriber's feed sorted set. This work happens off the request path, so a slow fan-out never slows the post button.

Comment threading, traced

Comments form arbitrarily deep trees, so we store them as an adjacency list: each comment row carries a parent_comment_id (NULL for a top-level comment) and the owning post_id. Crucially, comments are sharded by post_id, so one post's entire thread lives on one partition — the tree is fetched without scatter-gather.

Tracing a reply and a thread read:

  1. Reply. User B replies to comment C42 on post P. POST /posts/P/comments with parent_comment_id = 42 writes a row keyed to P's partition, bumps the denormalized comment_count on the post, and publishes CommentAdded{post_id:P, parent:42, author:B} for the notification service.
  2. Read the tree. GET /posts/P/comments issues a single partition read for all of P's comments, then rebuilds the tree in memory by linking each comment to its parent. Ordering within a sibling group reuses the same score-based rule the feed uses (top replies first), or chronological for "new".
  3. Hot threads. A post with tens of thousands of comments still reads from one partition; the top-level comments are cached separately so the common "first screen" render avoids materializing the whole tree.

Ranking: the corrected "hot" formula

The flagged bug is here. Reddit ranks a post by its net votes and its age, but the time term is the post's absolute creation timestamp, not an elapsed age. The published algorithm:

s     = ups - downs
order = log10(max(|s|, 1))
sign  = +1 if s > 0 else (-1 if s < 0 else 0)
t     = created_utc_seconds - 1134028003     # seconds since Reddit's epoch anchor (Dec 8 2005)
hot   = round(sign * order + t / 45000, 7)

Read t carefully: it is seconds-since-epoch of the post's creation. A newer post has a larger t and therefore a higher hot score. Two consequences drive the whole design:

Why the original was wrong: it used post_age = now - created with a + sign, which (a) makes older posts rank higher, backwards from the intent, and (b) makes every score a function of the current clock, forcing a full recompute of every post on every read and yielding an ordering that shifts continuously. Swapping the relative post_age for the absolute created_utc term fixes both.

diagram
diagram

Worked example: computing a hot score

Take Post P: 300 upvotes, 100 downvotes, created at created_utc = 1,780,000,000 (late May 2026).

  1. s = 300 - 100 = 200
  2. order = log10(max(200,1)) = log10(200) = 2.3010300
  3. sign = +1 (s > 0)
  4. t = 1,780,000,000 - 1,134,028,003 = 645,971,997
  5. t / 45000 = 14,354.9332667
  6. hot = round(1 × 2.3010300 + 14,354.9332667, 7) = 14357.2342967

Notice the time term (14,354.9) dwarfs the vote term (2.3). That is expected and fine: all posts being ranked share a nearly identical time magnitude, so what decides order at the margin is the difference in creation time plus the vote term.

Now Post Q — identical votes (300/100) but created 12.5 h later, created_utc = 1,780,045,000:

For the older Post P to tie Q, its order must rise by 1, i.e. log10(s_P) = 3.30103, so s_P = 10^{3.30103} = 2000. P needs a net 2000 votes to match Q's 200 — the promised 10×. This single arithmetic fact is the whole judgment layer of the ranking: a post has roughly a 12.5-hour window to gather each additional order of magnitude of votes before newer content structurally out-ranks it.

Feed delivery: push vs pull, with costs

Delivering feeds is the second correctness-critical choice. Two pure strategies sit at opposite ends of the write/read trade-off:

ModelWrite cost (per post)Read cost (per feed load)Best whenWorst when
Push (fan-out on write)O(subscribers) feed writesO(1) — read a pre-merged listread-heavy, modest fan-out, active readersmillion-subscriber producers (write storm); inactive readers (materialized work no one reads)
Pull (fan-out on read)O(1) — one row writeO(sources) query + merge every loadhuge fan-out; rarely-reading userslatency-sensitive default feed; users following many sources

Push makes reads trivial but pays a write proportional to the audience; pull makes writes trivial but pays a merge on every read. Neither alone survives Reddit's shape, where most communities are small but a handful (r/all, r/pics) have tens of millions of subscribers.

diagram
diagram

The hybrid decision rule and the celebrity fallback

The design is neither pure push nor pure pull — it is a hybrid keyed on a producer's fan-out. Let F be a subreddit's subscriber count (or, for user-follows, a user's follower count) and T a tuned threshold (order of tens of thousands to a low millions).

The rule:

Why this beats either pure model — the celebrity problem. A post to r/pics with 30M subscribers under pure push would trigger 30M feed writes, in a burst, most landing in feeds no one will read soon. Marking r/pics pull-only makes that post a single write, pulled on demand only by users who actually load a feed containing it. Pure pull, meanwhile, would force even a user's small-subreddit posts to be re-merged on every read; pushing those keeps the common case O(1).

When to use push / when not. Use push for the common case: modest fan-out, read-heavy, active readers who load feeds often (materialized work is amortized over many reads). Do not push for producers above the threshold (write amplification / bursts) or for dormant readers whose feeds would be materialized and never read — a last-active heuristic can suppress fan-out to inactive users and let them pull on return.

When to use pull / when not. Use pull for very-high-fan-out producers and for the long tail of inactive users. Do not make pull the default for active users' primary feed: an O(sources) merge on every load blows the low-latency (P99 < ~100 ms) target once a user follows many communities. The hybrid resolves this by pushing the many small sources and pulling only the few huge ones.

Sources

🤖 Don't fully get this? Learn it with Claude

Stuck on Designing Reddit? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🪜 Hint ladder (no spoilers)

Progressively stronger hints — you still solve it.

I'm working on the problem **Designing Reddit** (System Design). Give me a HINT LADDER: start with the tiniest nudge, then wait. Only reveal the next, stronger hint when I ask. Do NOT show the full solution unless I type 'show solution'. Keep me doing the thinking. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🎨 Explain the approach visually

See the technique, not just code.

Explain the optimal approach to **Designing Reddit** with a VISUAL walkthrough: trace it on a small concrete example using ASCII art / a step-by-step diagram, narrate what changes each step, then give time & space complexity with a one-line derivation. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🔍 Review my solution

Catch bugs, edge cases, sub-optimality.

I'll paste my solution to **Designing Reddit**. Review it for correctness, missed edge cases, and time/space complexity, then coach me toward the optimal — don't just rewrite it. Ask me to paste my code now. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🔁 Drill the pattern

Lock in recognition with look-alikes.

Give me 2 problems that use the SAME underlying pattern as **Designing Reddit**. For each, let me attempt first, then review my answer and name the trigger signal that reveals the pattern. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes