Social & Feed
Design Reddit
01
Scope of the platform
Scope of the platform
- Users create communities, join them, and submit text, link, image or video posts to a community.
- Every post and comment can be upvoted or downvoted once per user; the totals feed karma on the author's profile.
- Community pages sort posts by hot, new, top (by time window) and rising.
- A home feed blends posts from every community the user has joined; logged-out visitors get a popular feed.
- Threaded comments under each post, sortable and expandable.
- Moderators remove content, ban users and run automatic rules; anyone can report.
- Search across posts and communities.
- Feed and community pages load in p99 under 250 ms.
- Read-dominated: well over 95% of requests are page views.
- Vote totals may trail by a few seconds, but no vote is lost or counted twice.
- A single viral post must not degrade the rest of the site.
- New posts appear in "new" within seconds and can climb "hot" within minutes.
- Availability of 99.95% for reading; posting and voting may degrade first.
This is too big to design completely. Tell the interviewer you will focus on three hard parts: ranking a feed without scanning posts, serving comment trees, and counting votes under contention, and treat media, search and messaging as boxes you can name but not build out.
02
Back-of-envelope numbers
Back-of-envelope numbers
Assume 75 million daily active users, about 400,000 communities with activity in a given week, 1.1 million new posts and 9 million comments a day, and 90 million votes a day. A typical user loads about 12 feed or community pages a day.
| Quantity | Working | Result |
|---|---|---|
| Feed and community page loads | 75M x 12 per day | 900M a day, about 10,400 / s |
| Peak page loads | 3.5x average in the US evening | about 36,000 / s |
| Post detail views with comments | about 2 per feed page | about 72,000 / s at peak |
| Votes | 90M / 86,400 s, viral bursts 15x | 1,040 / s average, up to 15,000 / s |
| New posts | 1.1M / 86,400 s | about 13 / s |
| New comments | 9M / 86,400 s | about 105 / s |
| Post metadata per year | 1.1M x 365 x 1.5 KB | about 600 GB |
| Comments per year | 9M x 365 x 700 B | about 2.3 TB |
| Vote records per year | 90M x 365 x 30 B | about 1 TB |
| Media uploads | about 300K images and 40K videos a day | roughly 3 TB a day before transcoding |
| Hot lists in Redis | 400K communities x top 1,000 posts x about 40 B | about 16 GB |
Writes are tiny next to reads, but votes arrive in violent bursts on a handful of posts. Design the read path around precomputed ranked lists, and the write path around never letting one popular post lock a row.
03
Endpoints
Endpoints
04
System architecture
System architecture
flowchart LR
U["Web and apps"] --> CDN["CDN for media and logged-out pages"]
U --> GW["API gateway"]
GW --> LS["Listing service"]
GW --> PS["Post and comment service"]
GW --> VS["Vote service"]
LS --> RZ[("Redis: ranked lists per community")]
PS --> PG[("Postgres, sharded")]
VS --> VR[("Vote records, Cassandra")]
VS -. "vote events" .-> K[["Kafka"]]
PS -. "post and comment events" .-> K
K --> AG["Score aggregator"]
AG --> PG
AG --> RZ
K --> SR["Search indexer"]
K --> MOD["Spam and moderation"]The site is a read path and a write path joined by an event log. On the read side, the listing service answers "give me the next 25 posts for this community and sort" from sorted sets in Redis, then hydrates post ids into full cards from a cache in front of Postgres. Logged-out traffic, which is a large share of views, is served mostly from the CDN with short TTLs.
On the write side, posts and comments go to Postgres through one service, and votes go to their own service with its own store. Neither updates ranked lists directly. Instead they emit events, and the score aggregator applies vote deltas in batches, recomputes hot scores and updates Redis. Search indexing, spam scoring, notifications and karma all hang off the same Kafka topics, so each can fall behind without stopping posts or votes.
One sorted set per community and sort order, trimmed to the top 1,000 entries. A page read is a range query by rank, a few hundred microseconds in Redis.
Postgres sharded by community for posts and by post for comments. Holds titles, bodies, counts and per-sort scores. It is the truth that every cache can be rebuilt from.
A wide-column table keyed by user and thing. It answers "did this user vote, and which way" and is the record every count is derived from.
Clients upload straight to object storage; a worker scans, transcodes and thumbnails, and the CDN serves the results. The application servers never touch the bytes.
05
Deep dive: ranking, trees and vote counting
Deep dive: ranking, trees and vote counting
1. Hot ranking without rescoring
"Hot" has to favour posts that are both well liked and recent. A naive formula divides score by age, which means every post's value changes every second and the whole list needs constant rescoring. The trick in Reddit's published ranking code is to make time an additive term that never changes for a given post:
Because the time term uses the creation timestamp, a post's hot value only changes when its votes change. Newer posts automatically start higher, since their timestamp is larger, so older posts sink without anyone touching them. Every 45,000 seconds, about 12.5 hours, of age is worth one order of magnitude of votes: a post from this morning needs roughly ten times the net score to sit level with one made now.
That property is what makes precomputation possible. Store each post in a Redis sorted set for its community with the hot value as the member's score. On each batch of votes, recompute and ZADD it; when a post is created, add it once. A page is a ZREVRANGE by rank. Trim each set to the top 1,000, since nobody pages past that, and rebuild it from Postgres if Redis loses it.
"New" orders by creation time and never changes. "Top this week" keeps one sorted set per window, scored by net votes, with posts expiring out as they age past the window. "Rising" compares a post's recent vote rate with its community's normal rate, and is computed by a periodic job over the last few hours rather than per vote.
2. The home feed: merge at read time
A user who joined 60 communities needs one blended list. Writing every new post into every member's feed would mean fan-out to millions of members for big communities, and posts change rank constantly after they are written. So the home feed is pulled, not pushed: read the top 50 from each joined community's hot set in one pipelined Redis call, merge them by hot value with a heap, apply per-community caps so one busy community cannot fill the page, and cache the merged result for that user for about a minute. Users who joined thousands of communities get a sampled subset per request, which nobody notices.
3. Comment trees
Each comment row stores its parent id and a materialised path such as 0f3.1a9.22c, built from the parent's path plus its own id. Comments never change parent, so paths never need rewriting. Two queries cover everything a page needs: "the best k children of this comment", served by an index on post, parent and sort score, and "this comment and everything under it", served by a prefix match on path.
The first page of a thread is built breadth first with a node budget: the best 40 top-level comments, the best few replies under each, fewer again at the next level, until roughly 200 nodes are chosen. Anything cut off becomes a "load more" stub. "Best" uses the lower bound of a Wilson confidence interval on the upvote share, so a comment with 5 up and 0 down does not outrank one with 900 up and 100 down. That first page is cached per post and sort, and the reader's own votes are merged in afterwards.
4. The vote pipeline
flowchart LR
C["Vote request"] --> V["Vote service"]
V --> R[("Vote record: user, thing, dir")]
R --> D["Delta: new dir minus old dir"]
D --> K[["Kafka, keyed by thing id"]]
K --> A["Aggregator sums deltas per 2 s window"]
A --> P[("Postgres: one update per thing per window")]
A --> Z[("Redis: new hot value")]
A --> KM["Karma updater, async"]The vote service does one strongly ordered thing: it reads the user's existing vote on that thing, writes the new one, and emits the difference. Changing an upvote to a downvote emits minus two for ups minus downs; repeating the same vote emits nothing. That makes double clicks and retries harmless before anything is counted.
Events are partitioned by thing id, so all deltas for one post reach one aggregator instance in order. It keeps running sums in memory and flushes every two seconds, issuing a single update per post no matter whether it received 3 votes or 30,000 in that window. It then recomputes the hot value and writes it to Redis. Offsets are committed only after the flush, and each flush carries a window id so a replay after a crash cannot apply the same window twice.
The vote records are the ledger; the counters are a cache of it. If a counter is ever wrong, a job can recount from the records for that thing and overwrite it. Designs that keep only counters have nothing to repair from.
06
Common mistakes
Common mistakes
An ORDER BY over a computed hot expression must read every candidate post, and it runs tens of thousands of times a second at peak. The database spends its life recomputing a list that barely changed.
Large communities have tens of millions of members, so one post becomes tens of millions of writes, and those precomputed entries go stale as soon as the post's rank moves.
A post receiving thousands of votes per second turns one row into a lock queue. Latency for every write on that shard climbs with it.
A 300-comment page becomes 300 queries, each with its own round trip, and the deepest branch decides the latency.
Early comments collect votes simply by being seen first, and a small sample with no downvotes looks better than a large, mostly positive one.
07
Decisions and their costs
Decisions and their costs
Pages read in microseconds and the database is off the hot path. A post's new position appears a couple of seconds after the votes that moved it, which nobody perceives.
Fast reads, tiny stalenessAlways exact, but it costs a scan per view. Acceptable only for low-traffic sorts such as a moderator's queue.
Too costly per viewNo fan-out on post, rankings are always current, and adding a big community costs nothing extra. The price is a merge on each uncached request, roughly 60 small Redis reads pipelined into one round trip.
Fits community-based feedsWorks well for follow graphs where most accounts have few followers. Here membership is extremely skewed and rank changes after posting, so the inboxes would be both huge and stale.
Better for follow graphsThe user's own vote is read straight from the vote store, so their arrow is always right. The public number catches up within seconds, and displayed totals can be lightly fuzzed to frustrate manipulation.
Correct where users lookBuys precision nobody can see, at the cost of hot-row locking on exactly the posts with the most traffic.
Wrong place for strictnessPosts and comments need transactions, secondary indexes and moderation queries, which suit Postgres. Votes are a huge, simple key-value workload that suits Cassandra or a similar store.
Store per access patternOperating two databases and keeping their data consistent through events is real work. A small team could start with votes in Postgres and split later.
Split when volume demands08
Failure modes
Failure modes
Thousands of aged accounts upvote one link within ten minutes, putting a scam on the front page of a large community.
Weight every vote by a trust score from account age, behaviour and network signals before it reaches the aggregator, detect bursts from correlated accounts, and keep the raw records so votes can be voided and recounted after the fact.A post reaches the top of the popular feed. Its card and its comment page are requested tens of thousands of times a second, and each cache expiry sends a wave of identical rebuilds to one Postgres shard.
Refresh hot entries in the background before they expire, collapse concurrent misses into one rebuild, put logged-out views behind the CDN, and add read replicas for the shard that owns the post.A reply chain reaches thousands of levels. Paths become very long and renderers on older phones crash.
Limit depth per page with "continue this thread" stubs, cap path length by encoding ids compactly, and rate-limit replies within a single chain.Accounts repost old popular content to build karma, then sell themselves to spammers who need trusted-looking accounts.
Detect reposts with image hashes and link history, compute karma asynchronously so it can be adjusted, and avoid using karma alone as a trust signal anywhere it matters.An aggregator dies after writing a flush to Postgres but before committing its Kafka offset. On restart it replays the window and some posts gain votes twice.
Tag each flush with its window id and partition, store the last applied id per thing, and skip windows already applied. Run a nightly recount from vote records for the most active things as a safety net.A major event triples posting volume and the search indexer falls an hour behind, so people searching for the event find nothing new.
Scale indexer consumers on lag, prioritise new documents over score updates in the indexing queue, and fall back to the community's "new" listing for very recent results.09
How to handle the interview
How to handle the interview
Pick your three hard parts and say so. Ranking, comment trees and vote counting are where the depth is. Scoping out media and messaging early buys you time for the parts that score.
Explain why the hot formula allows precomputation. The key insight is that age enters as a fixed term per post, so scores only change on votes. Write the formula and show what 12.5 hours is worth.
Argue pull versus push for the home feed. Skewed community sizes and shifting ranks make pull the better fit here; contrast it with a follow-based feed to show you know when the answer flips.
Walk a vote from click to rank. Record, delta, partitioned stream, batched flush, new hot value. Point out where retries and crashes are made harmless.
Give the comment tree a budget. Per-level limits, stubs and a confidence-based "best" sort cover most follow-up questions.
Raise manipulation yourself. Vote trust, fuzzed counts and repairable ledgers show you have thought about adversaries, which any voting system attracts.
Separate logged-in and logged-out traffic. Anonymous pages are identical for everyone, so a CDN can carry a large share of total load cheaply.
10
How it grows
How it grows
Posts, comments and votes live in Postgres and pages sort with queries. At a few thousand users this is fast enough and far simpler to change.
Hot scores move into sorted sets updated on vote, rendered pages are cached, and logged-out traffic shifts to a CDN.
Votes get their own service and store, deltas flow through Kafka, and an aggregator batches updates. Search, notifications and spam detection subscribe to the same events.
Postgres shards by community and post, uploads go directly to object storage, and transcoding runs in its own worker fleet.
The home feed adds a learned ranker over the merged candidates, trust scoring weights every vote, and services split further along team lines.
Watch and read
Further watching and reading
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.