System design for Reddit: pre-computed hot feeds, Redis-buffered vote counting at 100M DAU scale, and N+1-free comment trees via recursive CTE.
What is Reddit?
Reddit is a community-driven platform where users submit posts (links or text) to topic communities called subreddits, and members vote to surface the best content. The engineering challenge is not storing posts. It is serving a dynamically ranked feed to millions of simultaneous users, counting votes under extreme concurrency without write contention, and rendering deeply nested comment trees without hammering the database on every page load.
This is a strong interview question because it tests three independent hard problems: feed pre-computation and ranking, high-concurrency write buffering, and tree data structures at scale. Each problem has a wrong-but-plausible solution that falls apart under load.
The interesting engineering is in the boundaries: how the Vote Flush Worker bridges Redis and PostgreSQL, how the recursive CTE avoids N+1 queries, and what breaks first when a post goes viral.
TL;DR
Keep posts, comments, and the canonical one-vote-per-user record in PostgreSQL. Pre-compute subreddit rankings in Redis sorted sets, buffer hot-post vote deltas in Redis, and flush them to PostgreSQL in batches. Serve comment trees with one depth-capped recursive CTE and a per-post cache. This favors feed availability and fast reads while accepting a few seconds of vote-count and ranking staleness.
Scope and assumptions
- The design covers post submission, subreddit feeds, votes on posts/comments, and nested comments; authentication is assumed at the gateway.
- A single primary region with PostgreSQL read replicas and Redis is enough for the baseline. Multi-region writes are a follow-up because vote uniqueness and feed updates need a conflict strategy.
- Feed pages use cursor pagination and support
hot,new, andtop. The hot ranking is pre-computed rather than sorted across the full posts table on every request. - Vote counts and feed scores may lag by up to five seconds, but the canonical
user_votesrecord must enforce one vote per user and target. - Link/text bodies are stored as application data; media hosting, search, moderation workflows, notifications, and profiles remain outside this core design.
Functional Requirements
Core Requirements
- Users can submit posts (link or text) to a subreddit.
- Users can upvote or downvote posts and comments.
- Users can view a subreddit feed sorted by hot, new, or top.
- Users can post comments on a post and reply to other comments.
Below the Line
- User authentication and account management.
- Push and email notifications.
- Full-text search across posts and comments.
- Video hosting and media transcoding.
- User profile pages and karma tracking.
The hardest part in scope: Serving a dynamically ranked "hot" feed to 100M daily users across 100K subreddits without recomputing scores on every request. The design must make the ranking a derived, continuously refreshed index rather than a full-table sort on every read.
Notifications are below the line because they require a separate event bus, push delivery infrastructure (APNs, FCM), and a preference store that sits orthogonal to the read and write path we are designing. To add them, we would publish a Kafka event on each post or vote and have a notification service consume and fan out based on subscription rules.
Full-text search is below the line because it requires a separate search index (Elasticsearch), a background ingestion pipeline, and query infrastructure that operates beside, not inside, the core path.
Video hosting is below the line because it demands a transcoding pipeline, adaptive bitrate streaming, and CDN delivery contracts that constitute a separate product surface.
Non-Functional Requirements
Core Requirements
- Availability: 99.99% uptime. Availability over consistency for feed and vote count reads.
- Feed latency: Subreddit feed and front page load under 200ms p99.
- Vote latency: Vote write acknowledged under 50ms p99. Vote count reflects updates within 5 seconds.
- Scale: 100M DAU, 500M total posts, 5B total comments. Roughly 5 new posts and 60 votes per second at steady state.
- Comment latency: Comment tree for a post with 1,000 comments loads under 300ms p99.
Feed latency under 200ms rules out sorting millions of posts per request at the database layer. A full ORDER BY hot_score across 500M rows would take multiple seconds, not milliseconds. Pre-computation is non-negotiable.
State this constraint early: pre-computation is non-negotiable because it explains why a Redis sorted set exists instead of a database sort on every request.
Vote counts are eventually consistent within 5 seconds. The vote count a user sees may lag by a handful of votes during burst traffic. This constraint is what unlocks write buffering and avoids row-level lock contention on hot posts.
Below the Line
- Sub-second real-time vote count updates pushed to the browser.
- Globally distributed multi-region writes.
Sub-second push updates require WebSocket infrastructure (or SSE), a pub/sub broadcast layer, and per-client connection state. All of this sits outside the read and write path here. The 5-second eventual consistency NFR is sufficient and keeps the design tractable.
Multi-region writes require a distributed transaction protocol or CRDT design for vote counting and feed updates. Both substantially complicate the Vote Flush Worker mechanism. Treat multi-region as a follow-on after the single-region design is solid.
Read/write ratio: For every post submitted, expect roughly 2,000 feed impressions and 5,000 individual post views. Votes come in at roughly 10x the post creation rate. The overall read-to-write ratio is around 200:1. Call this out early because every caching decision in the design traces back to this ratio.
30-second answer
Use PostgreSQL as the source of truth for posts, comments, and user votes. Put each subredditβs hot ranking in a Redis sorted set, update it asynchronously, and hydrate page IDs in one batched read. Accept votes quickly by writing the user vote durably and buffering score deltas in Redis; a worker flushes aggregate deltas every few seconds. Fetch comments with a depth-capped recursive CTE and cache the result. This gives fast, highly available reads while making vote counts eventually consistent.
5-minute explanation
- Set the contract. Support post creation, ranked/new/top feeds, votes, and replies. Target sub-200ms feed reads, sub-50ms vote acknowledgement, and bounded five-second freshness.
- Use the traffic shape. Reads dominate writes, so a database sort on every feed request is not viable. The hot feed is a materialized Redis sorted set keyed by subreddit.
- Separate canonical and derived state. PostgreSQL stores posts, comments, and the unique user vote row. Redis stores feed indexes, short-lived comment trees, and high-rate vote deltas.
- Walk the paths. A post inserts into PostgreSQL and seeds the feed; a feed reads ranked IDs then hydrates posts in bulk; a vote upserts the user vote and increments a Redis delta; a comment inserts and invalidates its postβs tree cache.
- Close the hard parts. The Vote Flush Worker batches score updates and can reconcile from
user_votes; the recursive CTE has an explicit depth cap; viral subreddits are isolated by cache keys and worker capacity.
45-minute interview approach
- 0β3 min β Clarify scope. Confirm whether the exercise covers only subreddit feeds or also a personalized home feed, what vote freshness is acceptable, maximum comment depth, and whether media/search/moderation are out of scope.
- 3β8 min β Establish the numbers. Write down DAU, post/vote rates, feed p99, comment-tree size, cache hit expectations, and the read-heavy ratio. Use these numbers to justify pre-computation and buffering.
- 8β13 min β Define entities and APIs. Sketch
Post,Comment,Vote,Subreddit, andUser, then show post, vote, feed, and comment endpoints with cursor pagination and idempotent vote semantics. - 13β22 min β Draw the baseline. Show the app tier, PostgreSQL primary/read replica, Redis feed cache, vote buffer, and comment cache. Walk through post creation and a hot-feed read end to end.
- 22β35 min β Prioritize the hard parts. Spend the largest block on hot-feed ranking, viral-post vote contention, one-vote uniqueness, the flush workerβs failure/reconciliation behavior, and comment-tree traversal without N+1 queries.
- 35β40 min β Cover reliability, security, and operations. Discuss Redis loss, database failover, cache rebuilds, moderation/auth boundaries, XSS-safe content handling, and metrics for flush lag and cache health.
- 40β44 min β Compare alternatives. Contrast database sorting with materialized rankings, synchronous vote updates with buffered deltas, and adjacency-list CTEs with materialized paths or document trees.
- 44β45 min β Recap and invite follow-ups. Restate what is canonical, what is derived, the allowed staleness, and the first bottleneck if a post goes viral.
Core Entities
- Post: A submission to a subreddit with a title, content (link URL or text body), vote score, comment count, and a creation timestamp.
- Comment: A reply to a post or another comment. Stores a
parent_comment_idreference for the tree structure. - Vote: A single upvote or downvote cast by one user on one post or comment. Enforces one vote per (user, target) pair.
- Subreddit: A named community with a subscriber count and display metadata.
- User: An account with a username and a karma score derived from post and comment votes received.
Schema details and index choices come up in the deep dives. The above is the minimal inventory needed to reason about the API and the data flow.
API Design
FR 1 - Submit a post:
# FR 1: Submit a new post to a subreddit
POST /r/{subreddit}/posts
Body: { title, content_type: "link" | "text", content }
Response: { post_id, permalink }
POST creates a new resource with a server-assigned ID. Return the full permalink (for example, /r/programming/comments/abc123/title-slug) rather than just the post_id so the client does not need to reconstruct it.
FR 2 - Vote on a post or comment:
# FR 2: Cast or change a vote
POST /posts/{post_id}/vote
Body: { direction: "up" | "down" | "none" }
Response: { new_score }
POST /comments/{comment_id}/vote
Body: { direction: "up" | "down" | "none" }
Response: { new_score }
direction: "none" removes an existing vote without replacing it. The endpoint is idempotent: calling it twice with the same direction produces the same result. Returning new_score lets the client update the displayed count immediately, without a separate GET.
FR 3 - View a subreddit feed:
# FR 3: Paginate the ranked feed for a subreddit
GET /r/{subreddit}/posts?sort=hot|new|top&after={cursor}
Response: { posts: [...], next_cursor: "..." }
Cursor-based over offset-based pagination: a cursor encodes the last post's score and ID so the server resumes exactly. Offset pagination on a dynamically re-ranked feed causes skipped or duplicated posts as scores shift between page loads.
FR 4 - Post and view comments:
# FR 4a: Post a comment
POST /posts/{post_id}/comments
Body: { body, parent_comment_id? }
Response: { comment_id, created_at }
# FR 4b: Fetch the comment tree
GET /posts/{post_id}/comments?sort=top|new
Response: { comments: [...] }
parent_comment_id is null for top-level comments and the parent's ID for replies. The response returns a flat list ordered by tree position. The client reconstructs the nested display from parent_comment_id fields.
High-Level Design
1. Users can submit a post
The write path: validate the submission, store the post row, and add the post to the subreddit's feed index.
Treat post ID generation as a black box for this design. The counter-based approach from the Pastebin design applies here: an atomic Redis counter encodes to a short base62 string, guaranteeing uniqueness without retry logic.
Components:
- Client: Web browser or mobile app sending HTTP requests.
- App Server: Validates content size, checks the subreddit exists, generates a post_id, inserts the post row, and seeds the feed index.
- PostgreSQL: Stores posts with title, content, author_id, subreddit_id, score (starting at 0), and created_at.
Request walkthrough:
- Client sends
POST /r/{subreddit}/postswith the title and content. - App Server validates content size and confirms the subreddit exists.
- App Server generates a post_id and inserts the post row into PostgreSQL.
- App Server returns the permalink to the client.
This diagram is intentionally minimal. Feed ranking, caching, and vote counting all come in the next requirements.
2. Users can view the subreddit feed
The feed read path carries the majority of all traffic. Sorting millions of posts on the database per request is not viable.
The naive approach: every GET /r/{subreddit}/posts?sort=hot runs ORDER BY hot_score DESC against the PostgreSQL posts table. At a few hundred posts per subreddit this is fast. At 100K subreddits each holding millions of posts, a full-table sort on every request exhausts CPU and I/O long before you reach 100M DAU.
The fix is to pre-compute a per-subreddit feed and store it as a Redis sorted set keyed feed:{subreddit}:{sort}. The score is the hot rank. Feed reads become O(log N + K) range queries against the sorted set instead of a full sort on disk.
Draw this transition explicitly: cross out the naive ORDER BY arrow and point it to Redis instead. The important part is the reasoning from the latency target to a materialized ranking.
The hot score formula is worth understanding: hot_score = sign(score) * log10(max(|score|, 1)) + created_at_epoch / 45000. Every order of magnitude increase in votes adds one rank point. Every 45,000 seconds (roughly 12.5 hours) of age adds one rank point, so a new post with a handful of votes competes fairly with old content that has many.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.