How to Design Community Platform like Reddit
Building subreddits, threaded discussions, voting, and content ranking at 1.7B+ monthly visit scale
1. Introduction — The Reddit Scale
Reddit is the self-proclaimed "front page of the internet" and one of the most visited websites on the planet. With 1.7 billion+ monthly visits, 100,000+ active subreddits, and 52 million+ daily active users, it represents one of the most complex community platforms ever built. From a simple link aggregator in 2005, Reddit has evolved into a sprawling ecosystem of threaded discussions, multimedia posts, live events, and real-time chat — all governed by a sophisticated karma and moderation system.
Designing a Reddit-like platform is a classic system design interview question because it touches virtually every distributed systems concept: eventual consistency for vote counts, nested data structures for comment threads, real-time feeds with ranking algorithms, content moderation at scale, and multi-tenant isolation through subreddits. This article walks you through every major component — from the data model to the ranking algorithms, from database sharding to multi-region failover — and concludes with a full 300+ line C# implementation.
Why This Matters for Senior+ Engineers
At the senior and staff level, you are expected to reason about trade-offs across the entire stack: consistency vs availability, latency vs throughput, storage cost vs read performance. Reddit's architecture forces you to confront all of these tensions simultaneously. Understanding how Reddit handles 1.7B monthly visits with sub-second feed loads prepares you for designing any large-scale social platform.
Key Numbers at Reddit Scale
| Metric | Value | Implication |
|---|---|---|
| Monthly Visits | 1.7 Billion+ | ~650K requests/sec peak |
| Daily Active Users | 52 Million+ | Massive read-heavy workload |
| Posts Per Day | ~1.5 Million | High write throughput for posts |
| Comments Per Day | ~16 Million | Comment tree is the hot path |
| Active Subreddits | 100,000+ | Multi-tenant isolation needed |
| Votes Per Second (peak) | ~250,000 | Write-heavy vote pipeline |
| Average Post Size | ~2 KB text + media ref | Media stored externally |
| Comment Depth | Average 4–6 levels | Nested tree with collapse |
The design must accommodate a read-to-write ratio of approximately 100:1, with extremely hot paths for feed reads and comment loading, and a surprisingly write-intensive voting system that must update ranking scores in near real-time.
2. Requirements Clarification
Functional Requirements
- User Management: Registration, login, profile pages, karma display, premium status
- Subreddit Creation: Users create communities with rules, flairs, moderators, and settings
- Post Creation: Text, link, image, video, gallery, poll, and AMA post types
- Comment Threading: Nested comments with collapse, expand, sort options
- Voting: Upvote/downvote on posts and comments with karma accumulation
- Feed Ranking: Hot, New, Top (hour/day/week/month/all), Controversial, Rising
- Search: Full-text search across posts, comments, subreddits, and users
- Moderation: Ban users, remove posts, auto-moderator rules, report system
- Notifications: Reply notifications, mentions, messages, moderator alerts
- Awards & Premium: Reddit Gold/Platinum equivalents, ad-free experience
- Media Upload: Image hosting, video hosting, GIF support, galleries
- Real-Time: Live threads for events, live chat in subreddits
Non-Functional Requirements
| Requirement | Target | Notes |
|---|---|---|
| Availability | 99.99% | ~53 min downtime/year |
| Feed Latency (p99) | < 200ms | Cached feeds served from edge |
| Post Read Latency (p99) | < 300ms | Includes comment tree |
| Vote Latency (p99) | < 100ms | Optimistic UI + async backend |
| Consistency | Eventual | Strong for user auth, eventual for feeds |
| Durability | 99.999999% | Multi-region replication |
| Throughput | 1M+ reads/sec | Through caching layer |
| Write Throughput | 50K+ writes/sec | Posts + comments + votes |
3. Capacity Estimation & Back-of-Envelope
Traffic Estimates
Assume 200M daily active users (DAU), each making ~5 page views per day.
Read-heavy system: 95% reads, 5% writes.
Write QPS = 11,600 x 0.05 = ~580 writes/sec
Peak traffic (2x average):
Peak Write QPS = ~1,160 writes/sec
With caching: 95% cache hit = 1,100 uncached reads/sec at peak
Storage Estimates
| Entity | Daily Count | Size Each | Daily Storage | Annual Storage |
|---|---|---|---|---|
| Posts | 1.5M | 2 KB | 3 GB | ~1.1 TB |
| Comments | 16M | 1 KB | 16 GB | ~5.8 TB |
| Votes | 200M | 50 B | 10 GB | ~3.6 TB |
| User Profiles | 100K new | 2 KB | 200 MB | ~73 GB |
| Media Metadata | 1.5M | 1 KB | 1.5 GB | ~548 GB |
| Messages | 5M | 500 B | 2.5 GB | ~913 GB |
Storage Total
Text data alone: ~12 GB/day = ~4.4 TB/year. Media (images, video) adds 10-50x this amount. At Reddit's scale, total storage including media exceeds 100+ PB across all regions.
Bandwidth Estimates
Outbound (without cache): 11,000 reads/sec x 5 KB (avg response) = ~55 MB/s = ~440 Mbps
Outbound (with 95% cache hit): ~22 Mbps uncached
Media bandwidth dominates at scale. Video content alone can consume 10+ Gbps of egress. This is why Reddit offloads media to CDN-backed object storage.
Key Insight
The voting system is the hardest scaling challenge. With 200M votes/day, the write path must handle 250K votes/sec at peak while updating ranking scores in near real-time. This requires a dedicated vote ingestion pipeline with eventual consistency semantics.
4. Data Model Design
Entity Relationship Overview
Core Tables — PostgreSQL Schema
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(40) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
display_name VARCHAR(100),
avatar_url TEXT,
bio TEXT,
karma_post INTEGER DEFAULT 0,
karma_comment INTEGER DEFAULT 0,
karma_total INTEGER GENERATED ALWAYS AS (karma_post + karma_comment) STORED,
is_premium BOOLEAN DEFAULT FALSE,
premium_expires_at TIMESTAMPTZ,
is_suspended BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_active_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_users_username ON users(username);
CREATE TABLE subreddits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(21) UNIQUE NOT NULL,
title VARCHAR(100) NOT NULL,
description TEXT,
sidebar TEXT,
creator_id UUID REFERENCES users(id),
icon_url TEXT,
banner_url TEXT,
subscriber_count INTEGER DEFAULT 0,
post_count INTEGER DEFAULT 0,
is_nsfw BOOLEAN DEFAULT FALSE,
is_restricted BOOLEAN DEFAULT FALSE,
settings JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_subreddits_name ON subreddits(name);
CREATE INDEX idx_subreddits_subscribers ON subreddits(subscriber_count DESC);
CREATE TABLE posts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(300) NOT NULL,
body TEXT,
post_type VARCHAR(20) NOT NULL CHECK (post_type IN ('text','link','image','video','gallery','poll','ama')),
author_id UUID REFERENCES users(id),
subreddit_id UUID REFERENCES subreddits(id),
url TEXT,
media_refs JSONB DEFAULT '[]',
flair VARCHAR(50),
is_locked BOOLEAN DEFAULT FALSE,
is_pinned BOOLEAN DEFAULT FALSE,
is_nsfw BOOLEAN DEFAULT FALSE,
is_spoiler BOOLEAN DEFAULT FALSE,
upvote_count INTEGER DEFAULT 0,
downvote_count INTEGER DEFAULT 0,
score INTEGER GENERATED ALWAYS AS (upvote_count - downvote_count) STORED,
comment_count INTEGER DEFAULT 0,
hot_score DOUBLE PRECISION DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_posts_subreddit ON posts(subreddit_id, created_at DESC);
CREATE INDEX idx_posts_author ON posts(author_id, created_at DESC);
CREATE INDEX idx_posts_hot ON posts(hot_score DESC);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
body TEXT NOT NULL,
author_id UUID REFERENCES users(id),
post_id UUID REFERENCES posts(id),
parent_comment_id UUID REFERENCES comments(id),
depth INTEGER DEFAULT 0,
path LTREE,
is_deleted BOOLEAN DEFAULT FALSE,
is_stickied BOOLEAN DEFAULT FALSE,
upvote_count INTEGER DEFAULT 0,
downvote_count INTEGER DEFAULT 0,
score INTEGER GENERATED ALWAYS AS (upvote_count - downvote_count) STORED,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_comments_post ON comments(post_id, created_at);
CREATE INDEX idx_comments_parent ON comments(parent_comment_id);
CREATE INDEX idx_comments_path ON comments USING GIST(path);
CREATE TABLE votes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
target_id UUID NOT NULL,
target_type VARCHAR(10) NOT NULL CHECK (target_type IN ('post','comment')),
value SMALLINT NOT NULL CHECK (value IN (-1, 0, 1)),
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, target_id, target_type)
);
CREATE INDEX idx_votes_target ON votes(target_id, target_type);
CREATE INDEX idx_votes_user ON votes(user_id);
CREATE TABLE awards (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(50) UNIQUE NOT NULL,
description TEXT,
icon_url TEXT,
cost_coins INTEGER NOT NULL,
karma_bonus INTEGER DEFAULT 0,
is_premium_only BOOLEAN DEFAULT FALSE
);
CREATE TABLE user_awards (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
award_id UUID REFERENCES awards(id),
target_id UUID NOT NULL,
target_type VARCHAR(10) NOT NULL,
awarded_by UUID REFERENCES users(id),
message TEXT,
awarded_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_user_awards_user ON user_awards(user_id, awarded_at DESC);
CREATE TABLE subreddit_members (
user_id UUID REFERENCES users(id),
subreddit_id UUID REFERENCES subreddits(id),
role VARCHAR(20) CHECK (role IN ('member','moderator','admin')),
joined_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY(user_id, subreddit_id)
);
Why PostgreSQL Over NoSQL?
PostgreSQL excels here because of its support for LTREE (materialized paths for comment trees), JSONB (flexible settings and metadata), generated columns (auto-computed scores), and ACID transactions (critical for vote consistency). Reddit historically used PostgreSQL for core data and Cassandra for time-series workloads like vote feeds.
5. API Design
RESTful Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| POST | /api/v1/auth/register | Create account | No |
| POST | /api/v1/auth/login | Login, return JWT | No |
| GET | /api/v1/users/{username} | Get user profile | No |
| PUT | /api/v1/users/me | Update own profile | Yes |
| POST | /api/v1/subreddits | Create subreddit | Yes |
| GET | /api/v1/subreddits/{name} | Get subreddit info | No |
| POST | /api/v1/subreddits/{name}/subscribe | Join/leave | Yes |
| POST | /api/v1/subreddits/{name}/posts | Create post | Yes |
| GET | /api/v1/subreddits/{name}/posts | List posts (sorted) | No |
| GET | /api/v1/posts/{id} | Get post + comments | No |
| POST | /api/v1/posts/{id}/comments | Add comment | Yes |
| POST | /api/v1/votes | Cast vote | Yes |
| GET | /api/v1/feed/home | Personalized home feed | Yes |
| GET | /api/v1/feed/popular | Popular across Reddit | No |
| GET | /api/v1/search | Search posts/comments | No |
| POST | /api/v1/media/upload | Upload media | Yes |
| POST | /api/v1/reports | Report content | Yes |
| POST | /api/v1/awards/give | Give award | Yes |
Example: Create Post Request/Response
POST /api/v1/subreddits/programming/posts
Authorization: Bearer <jwt_token>
Content-Type: application/json
{
"title": "How to Design a Rate Limiter — A Deep Dive",
"body": "In this article, we explore sliding window algorithms...",
"post_type": "text",
"flair": "Article"
}
// Response 201 Created
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "How to Design a Rate Limiter — A Deep Dive",
"author": {
"username": "ayodhyya",
"karma": 45230
},
"subreddit": "programming",
"score": 0,
"comment_count": 0,
"created_at": "2026-07-14T10:30:00Z",
"permalink": "/r/programming/comments/a1b2c3d4/"
}
gRPC Internal APIs
For inter-service communication, we use gRPC with Protocol Buffers:
syntax = "proto3";
package reddit.voting;
service VotingService {
rpc CastVote(CastVoteRequest) returns (CastVoteResponse);
rpc GetUserVotes(GetUserVotesRequest) returns (GetUserVotesResponse);
rpc BulkUpdateScores(BulkUpdateRequest) returns (BulkUpdateResponse);
}
message CastVoteRequest {
string user_id = 1;
string target_id = 2;
TargetType target_type = 3;
int32 value = 4;
}
enum TargetType { POST = 0; COMMENT = 1; }
message CastVoteResponse {
bool success = 1;
int32 new_score = 2;
string vote_id = 3;
}
6. High-Level Architecture
Architecture Principles
- Service Decomposition: Each domain (posts, comments, votes, feeds) is an independent service that can be scaled, deployed, and failed independently.
- Event-Driven Communication: Services communicate asynchronously through Kafka for non-critical paths (feed updates, notifications, ranking). Synchronous gRPC is used only for user-facing requests requiring immediate consistency.
- Cache-First Reads: The majority of reads hit Redis before touching the database. Cache invalidation is event-driven via Kafka consumers.
- Write-Ahead for Votes: Votes are written to Kafka first, then aggregated asynchronously. This decouples the vote submission latency from score computation.
- Polyglot Persistence: PostgreSQL for relational data, Cassandra for high-throughput time-series writes (votes, timelines), Redis for caching and real-time feeds, Elasticsearch for search.
7. Post Creation & Storage Pipeline
Post creation is a multi-step pipeline that goes beyond a simple database insert. We must handle content validation, spam detection, media processing, and index updates — all while keeping the user-facing latency under 500ms.
Post Type Handling
| Post Type | Storage | Processing | Special Handling |
|---|---|---|---|
| Text | PostgreSQL body column | Markdown to HTML | Auto-save drafts |
| Link | URL in post table | OG metadata fetch | Link preview card |
| Image | S3 + metadata in JSONB | Resize, WebP conversion | NSFW classification |
| Video | S3 + transcode manifest | HLS transcode, thumbnails | Duration limits |
| Gallery | S3 array + metadata | Per-image processing | Max 20 images |
| Poll | JSONB options + votes table | Expiration timer | Results reveal mode |
| AMA | Text + scheduling metadata | Q&A pairing logic | Time-boxed commenting |
Content Validation Pipeline
public class PostValidationPipeline
{
private readonly IContentValidator _titleValidator;
private readonly ISpamDetector _spamDetector;
private readonly IImageClassifier _nsfwClassifier;
public async Task<ValidationResult> ValidateAsync(CreatePostRequest request)
{
var errors = new List<string>();
var titleResult = await _titleValidator.ValidateAsync(request.Title);
if (!titleResult.IsValid)
errors.AddRange(titleResult.Errors);
if (request.PostType == PostType.Link && !string.IsNullOrEmpty(request.Url))
{
if (!Uri.TryCreate(request.Url, UriKind.Absolute, out _))
errors.Add("Invalid URL format");
}
if (request.PostType == PostType.Image && request.MediaIds?.Any() == true)
{
foreach (var mediaId in request.MediaIds)
{
var nsfwScore = await _nsfwClassifier.ClassifyAsync(mediaId);
if (nsfwScore > 0.85) request.IsNsfw = true;
}
}
var spamScore = await _spamDetector.GetSpamScoreAsync(
request.Title, request.Body, request.AuthorId);
return new ValidationResult
{
IsValid = errors.Count == 0,
Errors = errors,
SpamScore = spamScore,
RequiresReview = spamScore > 0.6
};
}
}
9. Voting System & Karma Calculation
The voting system is the engine that powers Reddit's content ranking. Every user can upvote (+1), downvote (-1), or retract (0) their vote on any post or comment. Votes must be idempotent and must update the target's score in near real-time.
Vote Ingestion Pipeline
At peak, Reddit processes 250,000+ votes per second. Writing each vote directly to PostgreSQL would overwhelm the database. Instead:
- Redis Write-Through: The vote is immediately written to Redis as the source of truth for the current user's vote state. This ensures idempotency on rapid clicks.
- Kafka Buffer: A VoteChanged event is published to Kafka, decoupling the user-facing response from database writes.
- Batch Aggregator: A consumer groups votes by target_id and batch-updates PostgreSQL every 5 seconds.
- Score Recalculation: After batch update, the new score is written back to Redis and triggers feed cache invalidation.
Karma Calculation
public class KarmaCalculator
{
public int CalculatePostKarma(int totalUpvotes, int totalDownvotes)
{
int score = totalUpvotes - totalDownvotes;
if (score <= 0) return 0;
int karma = 0;
int remaining = score;
// Tier 1: 1-10 upvotes = 1 karma each
int tier1 = Math.Min(remaining, 10);
karma += tier1;
remaining -= tier1;
// Tier 2: 11-100 upvotes = 1 karma per 2 upvotes
int tier2 = Math.Min(remaining, 90);
karma += tier2 / 2;
remaining -= tier2;
// Tier 3: 100+ upvotes = 1 karma per 10 upvotes
karma += remaining / 10;
return karma;
}
public int CalculateCommentKarma(int totalUpvotes, int totalDownvotes)
{
int score = totalUpvotes - totalDownvotes;
if (score <= 0) return 0;
int karma = 0;
int remaining = score;
int tier1 = Math.Min(remaining, 50);
karma += tier1;
remaining -= tier1;
karma += remaining / 5;
return karma;
}
public double CalculateControversy(int upvotes, int downvotes)
{
if (upvotes + downvotes < 10) return 0;
int total = upvotes + downvotes;
double balance = (double)Math.Min(upvotes, downvotes) /
Math.Max(upvotes, downvotes);
return Math.Log10(total) * balance;
}
}
Anti-Abuse Measures
- Vote Fuzzing: Reddit adds random +/-3 "fuzz" to displayed vote counts to prevent bots from detecting if their votes counted.
- Speed Limits: Max 50 votes per minute per user. Exceeding this triggers a shadowban.
- Ring Detection: If a set of users consistently votes on each other's content, all votes from that group are nullified.
- New Account Throttling: Accounts less than 24 hours old have limited voting weight.
- IP Correlation: Multiple accounts voting from the same IP on the same content triggers investigation.
10. Feed Ranking Algorithms
Reddit's ranking algorithms are the heart of the content discovery experience. The "Hot" algorithm is the most famous, combining score, time decay, and comment activity to surface the most engaging content.
Hot Ranking Algorithm
Where votes = upvotes - downvotes, 45000 = ~12.5 hour half-life constant
The log10 function ensures that the difference between 10 and 100 votes matters more than between 10,000 and 10,010 votes. The time component pushes newer content upward, creating a natural rotation.
All Ranking Algorithms
public class FeedRanker
{
public double CalculateHotScore(int upvotes, int downvotes, DateTime createdAt)
{
int score = upvotes - downvotes;
double order = Math.Log10(Math.Max(Math.Abs(score), 1));
double sign = score > 0 ? 1.0 : score < 0 ? -1.0 : 0.0;
double seconds = createdAt.ToUniversalTime()
.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
return sign * order + (seconds / 45000.0);
}
public double CalculateTopScore(int upvotes, int downvotes, TimeWindow window)
{
int score = upvotes - downvotes;
double decayFactor = window switch
{
TimeWindow.Hour => 1.0,
TimeWindow.Day => 0.5,
TimeWindow.Week => 0.2,
TimeWindow.Month => 0.08,
_ => 1.0
};
return score * decayFactor;
}
public double CalculateRisingScore(
int upvotes, int downvotes, DateTime createdAt, int commentCount)
{
double ageHours = DateTime.UtcNow.Subtract(createdAt).TotalHours;
if (ageHours > 12) return 0;
int score = upvotes - downvotes;
double velocity = (score + commentCount * 2.0) / Math.Max(ageHours, 0.1);
double freshness = Math.Max(0, 12 - ageHours) / 12.0;
return velocity * freshness;
}
public double CalculateControversialScore(int upvotes, int downvotes)
{
if (upvotes + downvotes < 10) return 0;
int total = upvotes + downvotes;
double ratio = (double)Math.Min(upvotes, downvotes) / Math.Max(upvotes, downvotes);
return Math.Log10(total) * ratio;
}
}
public enum TimeWindow { Hour, Day, Week, Month, AllTime }
Ranking Comparison Table
| Algorithm | Primary Signal | Time Weight | Best For |
|---|---|---|---|
| Hot | Score magnitude + time | Log decay, 12.5h half-life | Default home feed |
| New | Created time only | None (pure time sort) | Discovering fresh content |
| Top | Net upvotes | Window-based filter | Best content of time period |
| Rising | Vote velocity | 12h exponential decay | Content gaining momentum |
| Controversial | Vote ratio balance | Magnitude weighting | Debate/discussion threads |
| Best | Wilson lower bound | None | Comment sorting |
11. Subreddit Management & Moderation
Subreddits are the fundamental organizational unit of Reddit. Each subreddit is an independent community with its own rules, moderators, flairs, and culture.
Moderation Hierarchy
AutoModerator Rules Engine
| Rule Type | Match Criteria | Actions |
|---|---|---|
| Keyword Filter | Title/body contains word/regex | Remove, report, flair |
| Domain Block | URL domain in blocklist | Remove + notify |
| Account Age | Account less than X days old | Remove, queue for review |
| Karma Threshold | Comment karma less than threshold | Remove, message user |
| Repost Detection | Similar title within 30 days | Remove, suggest original |
| Report Threshold | Reports greater than N within M minutes | Auto-remove, alert mods |
| Flair Requirement | Post missing required flair | Remove until flaired |
Ban System
public class ModerationService
{
public async Task<BanResult> BanUserAsync(
string moderatorId, string targetUserId,
string subredditId, BanRequest request)
{
var modRole = await _membershipService
.GetRoleAsync(moderatorId, subredditId);
if (modRole != ModeratorRole.Moderator &&
modRole != ModeratorRole.Creator)
throw new UnauthorizedException("Insufficient permissions");
var targetRole = await _membershipService
.GetRoleAsync(targetUserId, subredditId);
if (targetRole >= modRole)
throw new InvalidOperationException(
"Cannot ban a moderator of equal or higher rank");
var ban = new SubredditBan
{
Id = Guid.NewGuid(),
SubredditId = subredditId,
UserId = targetUserId,
BannedBy = moderatorId,
Reason = request.Reason,
Duration = request.Duration,
ExpiresAt = request.Duration.HasValue
? DateTime.UtcNow.Add(request.Duration.Value)
: null,
CreatedAt = DateTime.UtcNow
};
await _database.Bans.InsertAsync(ban);
await _postService.RemoveAllByUserInSubredditAsync(
targetUserId, subredditId);
return new BanResult { Success = true, BanId = ban.Id };
}
}
12. Search System
Reddit's search must handle millions of queries daily across posts, comments, subreddits, and users. The system combines full-text search with faceted filtering and autocomplete.
Search Ranking Signals
| Signal | Weight | Notes |
|---|---|---|
| TF-IDF (text relevance) | 0.35 | Term frequency x inverse document frequency |
| Title match boost | 0.20 | Title matches ranked higher than body |
| Recency | 0.15 | Exponential decay over 30 days |
| Score (votes) | 0.15 | Higher voted content preferred |
| Comment engagement | 0.10 | More comments = more relevant |
| Author reputation | 0.05 | Higher karma authors get small boost |
13. Content Feed Generation
Feed generation is the most read-intensive operation in the system. Every page load requires fetching, ranking, and rendering a personalized set of posts.
Feed Types and Their Generation Strategy
| Feed Type | Generation | Cache TTL | Personalization |
|---|---|---|---|
| Home | Pre-computed per user | 5 min | Subscriptions + karma-weighted |
| Popular | Global hot posts (non-NSFW) | 2 min | Geo-weighted |
| Subreddit | Hot/New/Top per subreddit | 1 min | Subreddit-specific |
| All | Everything (admin only) | 5 min | None |
| Rising | Velocity-based global | 5 min | None |
Cursor-Based Pagination
public class FeedCursor
{
public string Encode(FeedItem lastItem)
{
var payload = $"{lastItem.Id}|{lastItem.CreatedAt.Ticks}|{lastItem.Score}";
return Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
}
public FeedCursorData Decode(string cursor)
{
var bytes = Convert.FromBase64String(cursor);
var parts = Encoding.UTF8.GetString(bytes).Split('|');
return new FeedCursorData
{
LastId = parts[0],
LastTimestamp = new DateTime(long.Parse(parts[1])),
LastScore = double.Parse(parts[2])
};
}
}
public async Task<FeedResult> GetFeedAsync(
string userId, string sortType, string cursor, int limit = 25)
{
var cursorData = cursor != null ? _cursor.Decode(cursor) : null;
var cacheKey = $"feed:{userId}:{sortType}";
var cached = await _redis.GetAsync<List<FeedItem>>(cacheKey);
if (cached != null)
{
var offset = cursorData != null
? cached.FindIndex(f => f.Id == cursorData.LastId) + 1 : 0;
return new FeedResult
{
Items = cached.Skip(offset).Take(limit).ToList(),
NextCursor = cached.Count > offset + limit
? _cursor.Encode(cached[offset + limit - 1]) : null,
HasMore = cached.Count > offset + limit
};
}
var posts = await _database.QueryAsync<Post>(
BuildFeedQuery(userId, sortType, cursorData, limit + 1));
return new FeedResult
{
Items = posts.Take(limit).ToList(),
NextCursor = posts.Count > limit ? _cursor.Encode(posts.Last()) : null,
HasMore = posts.Count > limit
};
}
14. Real-Time Updates
Reddit supports real-time features including live comment updates, notification delivery, live threads for major events, and real-time chat in subreddits.
Event Channels
| Channel | Trigger | Audience | Frequency |
|---|---|---|---|
| post:{id}:comments | New comment on post | Users viewing that post | High |
| user:{id}:notifications | Reply, mention, message | Specific user | Medium |
| subreddit:{id}:new | New post in subreddit | Users browsing subreddit | Medium |
| post:{id}:votes | Score change | Users viewing post | Very High |
| livethread:{id} | Live thread update | Subscribed users | Burst |
| chat:{subreddit} | Chat message | Chat participants | Medium |
15. Award & Premium System
Reddit's award system provides monetization and social recognition. Users purchase coins with real money, then spend coins to give awards to posts and comments.
Award Tiers
| Award | Coins Cost | Karma Bonus | Premium Duration | Coins to Recipient |
|---|---|---|---|---|
| Silver | 100 | +10 | None | 0 |
| Gold | 500 | +100 | 1 week | 100 |
| Platinum | 1800 | +700 | 1 month | 700 |
| Community Award | Variable | +Variable | None | Variable |
| Mod Award | Free (mod only) | +25 | 1 week | 0 |
Transaction Flow
public class AwardService
{
public async Task<AwardResult> GiveAwardAsync(
string giverId, string targetId, string awardId)
{
var award = await _cache.GetAwardAsync(awardId);
var giver = await _userService.GetUserAsync(giverId);
if (giver.Coins < award.CostCoins)
throw new InsufficientCoinsException(
$"Need {award.CostCoins} coins, have {giver.Coins}");
await using var tx = await _database.BeginTransactionAsync();
try
{
await _database.ExecuteAsync(
"UPDATE users SET coins = coins - @Cost WHERE id = @Id",
new { Cost = award.CostCoins, Id = giverId });
var recipientId = await GetTargetAuthorAsync(targetId);
await _database.ExecuteAsync(
@"INSERT INTO user_awards
(user_id, award_id, target_id, target_type, awarded_by)
VALUES (@UserId, @AwardId, @TargetId, @TargetType, @AwardedBy)",
new { UserId = recipientId, AwardId = awardId,
TargetId = targetId, TargetType = GetTargetType(targetId),
AwardedBy = giverId });
if (award.KarmaBonus > 0)
{
await _database.ExecuteAsync(
@"UPDATE users SET karma_total = karma_total + @Bonus
WHERE id = @Id",
new { Bonus = award.KarmaBonus, Id = recipientId });
}
if (award.CoinsToRecipient > 0)
{
await _database.ExecuteAsync(
@"UPDATE users SET coins = coins + @Credit WHERE id = @Id",
new { Credit = award.CoinsToRecipient, Id = recipientId });
}
await tx.CommitAsync();
return new AwardResult { Success = true,
RemainingCoins = giver.Coins - award.CostCoins };
}
catch { await tx.RollbackAsync(); throw; }
}
}
16. Media Upload Pipeline
Reddit hosts billions of images, videos, and GIFs. The media pipeline handles upload, virus scanning, content moderation, transcoding, and CDN distribution.
Media Constraints
| Type | Max Size | Formats | Processing | Max Duration |
|---|---|---|---|---|
| Image | 20 MB | JPG, PNG, GIF, WebP | Resize to 3 sizes | N/A |
| Video | 1 GB | MP4, MOV, WebM | HLS transcode (360p/720p/1080p) | 15 min |
| GIF | 100 MB | GIF | Convert to MP4 + keep GIF | 60 sec |
| Gallery | 20 images | Any image format | Per-image processing | N/A |
17. Spam Detection & Content Policy
Spam is a persistent challenge for any community platform. Reddit faces spam from automated bots, coordinated inauthentic behavior, and human spammers.
Spam Detection Layers
Layer 1: Rule-Based Filters (Pre-Publish)
- Keyword blacklists (updated daily from global spam patterns)
- URL reputation checking via Google Safe Browsing API
- Account age and karma thresholds for posting
- Rate limiting: max 10 posts/hour for new accounts
Layer 2: ML Classification (Pre-Publish)
- Text classification model (BERT-based) trained on historical spam/ham
- Image spam detection using CNN classifier
- Link spam: domain reputation features + page content analysis
- Score threshold: posts below 0.3 spam probability pass; 0.3-0.7 goes to review queue; above 0.7 auto-removed
Layer 3: Behavioral Analysis (Post-Publish)
- User posting pattern analysis (burst posting, identical content across subreddits)
- Voting ring detection (users who always vote the same way)
- Comment quality scoring (generic/spammy comment patterns)
Layer 4: Community Moderation (Ongoing)
- Report system: user reports with threshold-based auto-removal
- AutoModerator rules: per-subreddit configurable rules
- Mod queue: human review for edge cases
- Admin escalation for site-wide threats
18. Recommendation Engine
The recommendation engine powers three key features: post recommendations, subreddit suggestions, and home feed personalization.
Feature Engineering
| Feature | Source | Update Frequency |
|---|---|---|
| Subreddit subscription set | User profile | Real-time |
| Voting history by category | Votes table | Daily batch |
| Comment activity | Comments table | Daily batch |
| Time-of-day activity pattern | Activity logs | Weekly batch |
| Content embedding (BERT) | Post titles/bodies | On publish |
| Author following graph | Social graph | Real-time |
Personalization Algorithm
public class PersonalizationRanker
{
public double CalculatePersonalizedScore(
Post post, UserProfile user, UserFeatures features)
{
double baseScore = post.HotScore;
double subscriptionBoost = features.Subscriptions
.Contains(post.SubredditId) ? 1.5 : 0.3;
double authorBoost = features.AuthorAffinities
.GetValueOrDefault(post.AuthorId, 0.5);
double categoryBoost = features.CategoryAffinities
.GetValueOrDefault(post.Category, 0.5);
double timeBoost = IsUserActiveHour(user, DateTime.UtcNow)
? 1.2 : 0.8;
double diversityPenalty = CalculateDiversityPenalty(
post, features.RecentPostEmbeddings);
if (post.IsNsfw && !user.ShowNsfw) return 0;
return baseScore * subscriptionBoost * authorBoost
* categoryBoost * timeBoost * diversityPenalty;
}
private double CalculateDiversityPenalty(
Post post, List<float[]> recentEmbeddings)
{
if (!recentEmbeddings.Any()) return 1.0;
double maxSimilarity = recentEmbeddings
.Max(e => CosineSimilarity(post.Embedding, e));
return maxSimilarity > 0.9 ? 0.3
: maxSimilarity > 0.7 ? 0.7 : 1.0;
}
}
20. Caching Strategy
Multi-Level Cache Architecture
Cache Keys and TTLs
| Cache Key Pattern | Store | TTL | Invalidation |
|---|---|---|---|
| post:{id} | Redis | 15 min | On vote/comment update |
| feed:{user_id}:home | Redis | 5 min | On new post in sub |
| feed:popular:hot | Redis | 2 min | Periodic refresh |
| subreddit:{name}:posts | Redis | 1 min | On new post |
| score:post:{id} | Redis | 5 min | On vote change |
| vote:{user_id}:{target_id} | Redis | 24 hours | On vote change |
| user:{id}:profile | Redis | 30 min | On profile update |
| search:autocomplete:{prefix} | Redis | 5 min | Periodic refresh |
Cache Stampede Prevention
When a hot cache key expires simultaneously under high traffic, thousands of requests hit the database at once. We prevent this with probabilistic early expiration: each request has a small probability of refreshing the cache before TTL expiry, spreading the refresh load over time. Additionally, we use mutex locks in Redis to ensure only one request regenerates a given cache key.
21. Multi-Region Design
Data Replication Strategy
| Data Type | Replication | Consistency | Conflict Resolution |
|---|---|---|---|
| User profiles | Async PostgreSQL streaming | Eventual (max 5s lag) | Last-write-wins |
| Posts | Async PostgreSQL streaming | Eventual (max 5s lag) | Origin-region wins |
| Comments | Async PostgreSQL streaming | Eventual (max 5s lag) | Origin-region wins |
| Votes | Cassandra multi-DC | Eventual (strong within DC) | LWW (Cassandra default) |
| Feed cache | Redis CRDT | Eventual (max 30s) | CRDT merge |
| Search index | Elasticsearch CCR | Eventual (max 30s) | N/A (read-only replicas) |
22. Cost Estimation
Infrastructure Cost Breakdown (Monthly)
| Component | Specification | Monthly Cost (USD) |
|---|---|---|
| Application Servers (EKS) | 200 x c6i.2xlarge (8 vCPU, 16GB) | $280,000 |
| PostgreSQL (RDS Multi-AZ) | 10 x db.r6g.4xlarge, 50TB storage | $120,000 |
| Redis Cluster (ElastiCache) | 500 x r6g.xlarge nodes | $200,000 |
| Cassandra (Keyspaces) | 500K RCU/WCU | $150,000 |
| Elasticsearch | 50 x r6i.2xlarge.search | $100,000 |
| Kafka (MSK) | 30 x kafka.m5.2xlarge | $60,000 |
| S3 Storage (Media) | 200TB + 50TB/month growth | $15,000 |
| CloudFront CDN | 5PB/month transfer | $400,000 |
| ALB + WAF | 10 ALBs + WAF rules | $25,000 |
| Route 53 + SSL | DNS + certificates | $5,000 |
| Background Workers | 100 x c6i.xlarge | $70,000 |
| Monitoring (CloudWatch) | Full observability stack | $50,000 |
| ML Infrastructure | Inference + training | $80,000 |
| Multi-Region (EU + APAC) | ~40% of US costs | $620,000 |
Total Monthly Cost: ~$2.17M
Annual Cost: ~$26M
Cost Per User (200M DAU): ~$0.001/day = $0.03/month
Revenue model: Reddit generates ~$1.3B annually from ads + premium. Cost per user of $0.03/month leaves healthy margins for an $800M+ revenue business.
23. Interview Q&A
Q1: How would you handle the hot post problem where a single viral post receives millions of votes?
A: Viral posts create a thundering herd problem. We handle this through: (1) Write-path: votes are ingested via Kafka, not direct DB writes, absorbing bursts. (2) Read-path: the post is pinned in Redis, avoiding repeated DB reads. (3) Score computation: hot scores are recalculated every 5 seconds in a batch aggregator, not on every vote. (4) CDN caching: the post page is served from CDN, absorbing 90%+ of read traffic. (5) Vote fuzzing: displayed score is approximate, reducing bot attack incentive.
Q2: How do you prevent a moderator from manipulating rankings by deleting and undeleting posts?
A: All moderation actions are logged in an immutable audit trail. Each action has a timestamp, moderator ID, and action type. Undeletion is possible but the audit log tracks the full history. Additionally, rank manipulation is detected by monitoring for patterns: a moderator repeatedly deleting/undeleting posts that benefit a specific user triggers an automated alert to site admins.
Q3: How would you design the notification system to handle millions of notifications per day?
A: We use a tiered notification system: (1) Real-time: WebSocket push for active users. (2) Batched: email digests sent hourly or daily based on user preference. (3) Intelligent batching: multiple replies to the same post are grouped into one notification. (4) Priority: mentions are high priority, distant replies are low. (5) Rate limiting: max 50 push notifications per hour per user. (6) DND scheduling: users set quiet hours.
Q4: How do you handle the eventual consistency of vote counts?
A: Vote counts are eventually consistent with up to 5-second lag. The user's own vote is immediately consistent (served from Redis). Other users' votes may be slightly delayed. This is acceptable because: (1) Reddit shows "score hidden" for the first hour on many subreddits. (2) The displayed score is already fuzzed. (3) Users care more about their own vote being registered correctly than seeing the exact real-time total.
Q5: How would you implement "collapse all comments" efficiently for deeply nested threads?
A: The collapse state is purely a frontend concern. The server returns the full comment tree with depth metadata, and the frontend renders only top-level comments initially. Collapsing is a UI state toggle. For very deep threads (10+ levels), we use the LTREE path to efficiently query subtrees. Lazy loading: when a user expands a collapsed thread, we fetch the subtree if not in the initial payload.
Q6: Design a system to detect and prevent vote manipulation rings.
A: Vote ring detection operates on multiple signals: (1) Temporal correlation: multiple accounts voting on the same content within a narrow window. (2) IP analysis: multiple accounts from the same IP voting on the same content. (3) Behavioral similarity: accounts that always upvote the same users. (4) Graph analysis: constructing a bipartite graph of voters-votees and finding densely connected components. (5) Account similarity: creation time clustering, username patterns. When detected, all votes are nullified and accounts suspended.
Q7: How do you handle cross-region read-your-writes consistency?
A: We use a "write token" approach. When a user writes from region EU, the response includes a token. For the next 30 seconds, reads from EU that include this token are forwarded to US East (the primary) instead of reading from the local replica. This provides read-your-writes consistency for the user's own actions while allowing all other reads to be served locally.
Q8: How would you scale the comment system to handle an AMA with 50,000+ comments?
A: AMAs are the highest write-intensity events. We handle this through: (1) Dedicated AMA cluster with isolated resources. (2) Comment tree pagination: only top 200 root comments loaded initially. (3) Q&A mode pairs OP answers with questions. (4) Redis caching: the comment tree is kept entirely in Redis. (5) Write buffering: comments batched in Kafka and written to DB in bulk. (6) Rate limiting for non-OP commenters.
Q9: How do you implement subreddit-specific rules while maintaining a global content policy?
A: Two-tier rule system: (1) Global policy: enforced centrally before content is published — site-wide bans, NSFW requirements, spam filtering, legal compliance (DMCA, CSAM detection). (2) Subreddit rules: enforced by AutoModerator after global checks pass. Subreddit rules can be more restrictive but never less restrictive than global policy. Rules evaluate in a pipeline: global, then subreddit, then custom AutoModerator.
Q10: How would you design data migration when introducing a new feature that requires backfilling?
A: We use the expand-and-contract migration pattern: (1) Add the new column/table alongside existing schema. (2) Dual-write: new code writes to both old and new fields. (3) Backfill: a background job migrates historical data in batches with progress tracking. (4) Verify: consistency checks comparing old and new fields. (5) Switch reads: gradually route traffic to new field using feature flags. (6) Cleanup: remove old column after a bake period.
Q11: How do you handle the "first comment" problem where early comments get disproportionate visibility?
A: Reddit mitigates this through: (1) "Controversial" sort that doesn't favor early comments. (2) Randomization: adding small random noise to comment scores in the first 30 minutes. (3) Contest mode: randomized sort with hidden scores. (4) Sort options: users can switch to "New" for recent comments. (5) "Read more" links for deeply nested but high-quality comments. The design accepts some first-comment advantage as a feature while providing tools to mitigate it.
Q12: Explain the trade-offs between pre-computed feeds and on-demand feed generation.
A: Pre-computed feeds: (Pros) Sub-50ms response times, reduced DB load during peak traffic. (Cons) Stale data up to TTL, high write amplification, memory-intensive Redis usage. On-demand generation: (Pros) Always fresh data, lower write amplification. (Cons) Higher latency (100-500ms), unpredictable DB load during traffic spikes. Reddit uses a hybrid: pre-computed for home feeds (freshness less critical), on-demand for subreddit feeds (smaller scope), and always fresh for search results.
24. Full C# Implementation
Production-grade C# implementation covering Post Service, Comment Service, Voting Service, Feed Service, Moderation Service, and Award Service with Redis caching, PostgreSQL persistence, and Kafka event publishing.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Npgsql;
using StackExchange.Redis;
using Confluent.Kafka;
namespace RedditPlatform.Core
{
public enum PostType { Text, Link, Image, Video, Gallery, Poll, Ama }
public enum VoteDirection { Downvote = -1, None = 0, Upvote = 1 }
public enum SortType { Hot, New, Top, Rising, Controversial, Best }
public enum TimeWindow { Hour, Day, Week, Month, AllTime }
public enum ModerationAction { Remove, Approve, Ban, Flag, Flair, Lock }
public class User
{
public Guid Id { get; set; }
public string Username { get; set; } = "";
public string Email { get; set; } = "";
public int KarmaPost { get; set; }
public int KarmaComment { get; set; }
public int KarmaTotal => KarmaPost + KarmaComment;
public bool IsPremium { get; set; }
public int Coins { get; set; }
public bool IsSuspended { get; set; }
public DateTime CreatedAt { get; set; }
}
public class Post
{
public Guid Id { get; set; }
public string Title { get; set; } = "";
public string? Body { get; set; }
public PostType PostType { get; set; }
public Guid AuthorId { get; set; }
public Guid SubredditId { get; set; }
public string? Url { get; set; }
public List<string> MediaRefs { get; set; } = new();
public string? Flair { get; set; }
public bool IsLocked { get; set; }
public bool IsPinned { get; set; }
public bool IsNsfw { get; set; }
public bool IsSpoiler { get; set; }
public int UpvoteCount { get; set; }
public int DownvoteCount { get; set; }
public int Score => UpvoteCount - DownvoteCount;
public int CommentCount { get; set; }
public double HotScore { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class Comment
{
public Guid Id { get; set; }
public string Body { get; set; } = "";
public Guid AuthorId { get; set; }
public Guid PostId { get; set; }
public Guid? ParentCommentId { get; set; }
public int Depth { get; set; }
public string Path { get; set; } = "";
public bool IsDeleted { get; set; }
public int UpvoteCount { get; set; }
public int DownvoteCount { get; set; }
public int Score => UpvoteCount - DownvoteCount;
public DateTime CreatedAt { get; set; }
}
public class Vote
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public Guid TargetId { get; set; }
public string TargetType { get; set; } = "post";
public VoteDirection Value { get; set; }
public DateTime CreatedAt { get; set; }
}
public class Award
{
public Guid Id { get; set; }
public string Name { get; set; } = "";
public string? Description { get; set; }
public string? IconUrl { get; set; }
public int CostCoins { get; set; }
public int KarmaBonus { get; set; }
public int CoinsToRecipient { get; set; }
}
public class ModerationLog
{
public Guid Id { get; set; }
public ModerationAction Action { get; set; }
public Guid ModeratorId { get; set; }
public Guid? TargetUserId { get; set; }
public Guid? TargetPostId { get; set; }
public Guid? TargetCommentId { get; set; }
public Guid SubredditId { get; set; }
public string? Reason { get; set; }
public DateTime CreatedAt { get; set; }
}
public record CreatePostRequest(
string Title, string? Body, PostType PostType,
string? Url, List<string>? MediaRefs,
string? Flair, bool IsNsfw, bool IsSpoiler);
public record CreateCommentRequest(string Body, Guid? ParentCommentId);
public record CastVoteRequest(Guid TargetId, string TargetType, VoteDirection Value);
public record FeedItem(Post Post, string SubredditName, string AuthorUsername,
string? AuthorAvatar, int AuthorKarma, VoteDirection? UserVote);
public record FeedResult(List<FeedItem> Items, string? NextCursor, bool HasMore);
public record VoteResult(bool Success, int NewScore, bool Changed);
public class PlatformConfig
{
public string DatabaseConnectionString { get; set; } = "";
public string RedisConnectionString { get; set; } = "";
public string KafkaBootstrapServers { get; set; } = "";
public int FeedCacheTtlMinutes { get; set; } = 5;
public int PostCacheTtlMinutes { get; set; } = 15;
public int MaxCommentDepth { get; set; } = 10;
public int FeedPageSize { get; set; } = 25;
}
public class FeedRanker
{
public double CalculateHotScore(int upvotes, int downvotes, DateTime createdAt)
{
int score = upvotes - downvotes;
double order = Math.Log10(Math.Max(Math.Abs(score), 1));
double sign = score > 0 ? 1.0 : score < 0 ? -1.0 : 0.0;
double seconds = createdAt.ToUniversalTime()
.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
return sign * order + (seconds / 45000.0);
}
public double CalculateTopScore(int upvotes, int downvotes, TimeWindow window)
{
int score = upvotes - downvotes;
double decay = window switch
{
TimeWindow.Hour => 1.0, TimeWindow.Day => 0.5,
TimeWindow.Week => 0.2, TimeWindow.Month => 0.08, _ => 1.0
};
return score * decay;
}
public double CalculateRisingScore(
int upvotes, int downvotes, DateTime createdAt, int commentCount)
{
double ageHours = DateTime.UtcNow.Subtract(createdAt).TotalHours;
if (ageHours > 12) return 0;
int score = upvotes - downvotes;
double velocity = (score + commentCount * 2.0) / Math.Max(ageHours, 0.1);
return velocity * Math.Max(0, 12 - ageHours) / 12.0;
}
public double CalculateControversialScore(int upvotes, int downvotes)
{
if (upvotes + downvotes < 10) return 0;
double ratio = (double)Math.Min(upvotes, downvotes) / Math.Max(upvotes, downvotes);
return Math.Log10(upvotes + downvotes) * ratio;
}
}
public class KarmaCalculator
{
public int CalculatePostKarma(int totalUpvotes, int totalDownvotes)
{
int score = totalUpvotes - totalDownvotes;
if (score <= 0) return 0;
int karma = 0, remaining = score;
int t1 = Math.Min(remaining, 10); karma += t1; remaining -= t1;
int t2 = Math.Min(remaining, 90); karma += t2 / 2; remaining -= t2;
karma += remaining / 10;
return karma;
}
public int CalculateCommentKarma(int totalUpvotes, int totalDownvotes)
{
int score = totalUpvotes - totalDownvotes;
if (score <= 0) return 0;
int t1 = Math.Min(score, 50);
return t1 + (score - t1) / 5;
}
}
public class PostService
{
private readonly PlatformConfig _config;
private readonly NpgsqlConnection _db;
private readonly IConnectionMultiplexer _redis;
private readonly IProducer<Null, string> _kafka;
private readonly FeedRanker _ranker = new();
public PostService(PlatformConfig config, NpgsqlConnection db,
IConnectionMultiplexer redis, IProducer<Null, string> kafka)
{
_config = config; _db = db; _redis = redis; _kafka = kafka;
}
public async Task<Post> CreatePostAsync(
Guid authorId, Guid subredditId, CreatePostRequest request)
{
var post = new Post
{
Id = Guid.NewGuid(), Title = request.Title, Body = request.Body,
PostType = request.PostType, AuthorId = authorId,
SubredditId = subredditId, Url = request.Url,
MediaRefs = request.MediaRefs ?? new(), Flair = request.Flair,
IsNsfw = request.IsNsfw, IsSpoiler = request.IsSpoiler,
CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow
};
post.HotScore = _ranker.CalculateHotScore(0, 0, post.CreatedAt);
await using var cmd = new NpgsqlCommand(@"
INSERT INTO posts (id, title, body, post_type, author_id,
subreddit_id, url, media_refs, flair, is_nsfw,
is_spoiler, hot_score, created_at, updated_at)
VALUES (@id, @title, @body, @post_type, @author_id,
@subreddit_id, @url, @media_refs::jsonb, @flair,
@is_nsfw, @is_spoiler, @hot_score, @created_at, @updated_at)", _db);
cmd.Parameters.AddWithValue("id", post.Id);
cmd.Parameters.AddWithValue("title", post.Title);
cmd.Parameters.AddWithValue("body", (object?)post.Body ?? DBNull.Value);
cmd.Parameters.AddWithValue("post_type", post.PostType.ToString().ToLower());
cmd.Parameters.AddWithValue("author_id", post.AuthorId);
cmd.Parameters.AddWithValue("subreddit_id", post.SubredditId);
cmd.Parameters.AddWithValue("url", (object?)post.Url ?? DBNull.Value);
cmd.Parameters.AddWithValue("media_refs", JsonSerializer.Serialize(post.MediaRefs));
cmd.Parameters.AddWithValue("flair", (object?)post.Flair ?? DBNull.Value);
cmd.Parameters.AddWithValue("is_nsfw", post.IsNsfw);
cmd.Parameters.AddWithValue("is_spoiler", post.IsSpoiler);
cmd.Parameters.AddWithValue("hot_score", post.HotScore);
cmd.Parameters.AddWithValue("created_at", post.CreatedAt);
cmd.Parameters.AddWithValue("updated_at", post.UpdatedAt);
await cmd.ExecuteNonQueryAsync();
var cache = _redis.GetDatabase();
await cache.KeyDeleteAsync($"subreddit:{subredditId}:hot");
await _kafka.ProduceAsync("post-events", new Message<Null, string>
{
Value = JsonSerializer.Serialize(new
{
EventType = "PostCreated", PostId = post.Id.ToString(),
AuthorId = authorId.ToString(), Title = post.Title
})
});
return post;
}
public async Task<Post?> GetPostAsync(Guid postId)
{
var cache = _redis.GetDatabase();
var cached = await cache.StringGetAsync($"post:{postId}");
if (cached.HasValue)
return JsonSerializer.Deserialize<Post>(cached!);
await using var cmd = new NpgsqlCommand(@"
SELECT id, title, body, post_type, author_id, subreddit_id,
url, media_refs, flair, is_locked, is_pinned, is_nsfw,
is_spoiler, upvote_count, downvote_count, comment_count,
hot_score, created_at, updated_at
FROM posts WHERE id = @id", _db);
cmd.Parameters.AddWithValue("id", postId);
await using var reader = await cmd.ExecuteReaderAsync();
if (!await reader.ReadAsync()) return null;
var post = new Post
{
Id = reader.GetGuid(0), Title = reader.GetString(1),
Body = reader.IsDBNull(2) ? null : reader.GetString(2),
PostType = Enum.Parse<PostType>(reader.GetString(3), true),
AuthorId = reader.GetGuid(4), SubredditId = reader.GetGuid(5),
Url = reader.IsDBNull(6) ? null : reader.GetString(6),
MediaRefs = JsonSerializer.Deserialize<List<string>>(reader.GetString(7)) ?? new(),
Flair = reader.IsDBNull(8) ? null : reader.GetString(8),
IsLocked = reader.GetBoolean(9), IsPinned = reader.GetBoolean(10),
IsNsfw = reader.GetBoolean(11), IsSpoiler = reader.GetBoolean(12),
UpvoteCount = reader.GetInt32(13), DownvoteCount = reader.GetInt32(14),
CommentCount = reader.GetInt32(15), HotScore = reader.GetDouble(16),
CreatedAt = reader.GetDateTime(17), UpdatedAt = reader.GetDateTime(18)
};
await cache.StringSetAsync($"post:{postId}",
JsonSerializer.Serialize(post),
TimeSpan.FromMinutes(_config.PostCacheTtlMinutes));
return post;
}
}
public class CommentService
{
private readonly PlatformConfig _config;
private readonly NpgsqlConnection _db;
private readonly IConnectionMultiplexer _redis;
private readonly IProducer<Null, string> _kafka;
public CommentService(PlatformConfig config, NpgsqlConnection db,
IConnectionMultiplexer redis, IProducer<Null, string> kafka)
{
_config = config; _db = db; _redis = redis; _kafka = kafka;
}
public async Task<Comment> CreateCommentAsync(
Guid authorId, Guid postId, CreateCommentRequest request)
{
string path = ""; int depth = 0;
if (request.ParentCommentId.HasValue)
{
var parent = await GetCommentAsync(request.ParentCommentId.Value);
if (parent == null)
throw new InvalidOperationException("Parent comment not found");
if (parent.Depth >= _config.MaxCommentDepth)
throw new InvalidOperationException($"Max depth exceeded");
path = $"{parent.Path}.{parent.Id}"; depth = parent.Depth + 1;
}
else { path = "0"; depth = 0; }
var comment = new Comment
{
Id = Guid.NewGuid(), Body = request.Body, AuthorId = authorId,
PostId = postId, ParentCommentId = request.ParentCommentId,
Depth = depth, Path = path, CreatedAt = DateTime.UtcNow
};
await using var cmd = new NpgsqlCommand(@"
INSERT INTO comments (id, body, author_id, post_id,
parent_comment_id, depth, path, created_at)
VALUES (@id, @body, @author_id, @post_id,
@parent_id, @depth, @path::ltree, @created_at)", _db);
cmd.Parameters.AddWithValue("id", comment.Id);
cmd.Parameters.AddWithValue("body", comment.Body);
cmd.Parameters.AddWithValue("author_id", comment.AuthorId);
cmd.Parameters.AddWithValue("post_id", comment.PostId);
cmd.Parameters.AddWithValue("parent_id",
(object?)comment.ParentCommentId ?? DBNull.Value);
cmd.Parameters.AddWithValue("depth", comment.Depth);
cmd.Parameters.AddWithValue("path", comment.Path);
cmd.Parameters.AddWithValue("created_at", comment.CreatedAt);
await cmd.ExecuteNonQueryAsync();
await using var countCmd = new NpgsqlCommand(@"
UPDATE posts SET comment_count = comment_count + 1,
updated_at = NOW() WHERE id = @id", _db);
countCmd.Parameters.AddWithValue("id", postId);
await countCmd.ExecuteNonQueryAsync();
var cache = _redis.GetDatabase();
await cache.KeyDeleteAsync($"post:{postId}");
await cache.KeyDeleteAsync($"comments:{postId}");
await _kafka.ProduceAsync("comment-events", new Message<Null, string>
{
Value = JsonSerializer.Serialize(new
{
EventType = "CommentCreated", CommentId = comment.Id.ToString(),
PostId = postId.ToString(), AuthorId = authorId.ToString()
})
});
return comment;
}
public async Task<List<Comment>> GetCommentTreeAsync(Guid postId)
{
var cache = _redis.GetDatabase();
var cached = await cache.StringGetAsync($"comments:{postId}");
if (cached.HasValue)
return JsonSerializer.Deserialize<List<Comment>>(cached!) ?? new();
var comments = new List<Comment>();
await using var cmd = new NpgsqlCommand(@"
WITH RECURSIVE tree AS (
SELECT id, body, author_id, post_id, parent_comment_id,
depth, path, upvote_count, downvote_count, created_at
FROM comments WHERE post_id = @postId
AND parent_comment_id IS NULL AND is_deleted = FALSE
UNION ALL
SELECT c.id, c.body, c.author_id, c.post_id,
c.parent_comment_id, c.depth, c.path,
c.upvote_count, c.downvote_count, c.created_at
FROM comments c JOIN tree t ON c.parent_comment_id = t.id
WHERE c.is_deleted = FALSE AND c.depth <= @maxDepth
) SELECT * FROM tree ORDER BY path", _db);
cmd.Parameters.AddWithValue("postId", postId);
cmd.Parameters.AddWithValue("maxDepth", _config.MaxCommentDepth);
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
comments.Add(new Comment
{
Id = reader.GetGuid(0), Body = reader.GetString(1),
AuthorId = reader.GetGuid(2), PostId = reader.GetGuid(3),
ParentCommentId = reader.IsDBNull(4) ? null : reader.GetGuid(4),
Depth = reader.GetInt32(5), Path = reader.GetString(6),
UpvoteCount = reader.GetInt32(7), DownvoteCount = reader.GetInt32(8),
CreatedAt = reader.GetDateTime(9)
});
}
await cache.StringSetAsync($"comments:{postId}",
JsonSerializer.Serialize(comments), TimeSpan.FromMinutes(10));
return comments;
}
private async Task<Comment?> GetCommentAsync(Guid id)
{
await using var cmd = new NpgsqlCommand(@"
SELECT id, body, author_id, post_id, parent_comment_id,
depth, path, upvote_count, downvote_count, created_at
FROM comments WHERE id = @id", _db);
cmd.Parameters.AddWithValue("id", id);
await using var r = await cmd.ExecuteReaderAsync();
if (!await r.ReadAsync()) return null;
return new Comment
{
Id = r.GetGuid(0), Body = r.GetString(1), AuthorId = r.GetGuid(2),
PostId = r.GetGuid(3),
ParentCommentId = r.IsDBNull(4) ? null : r.GetGuid(4),
Depth = r.GetInt32(5), Path = r.GetString(6),
UpvoteCount = r.GetInt32(7), DownvoteCount = r.GetInt32(8),
CreatedAt = r.GetDateTime(9)
};
}
}
public class VotingService
{
private readonly PlatformConfig _config;
private readonly NpgsqlConnection _db;
private readonly IConnectionMultiplexer _redis;
private readonly IProducer<Null, string> _kafka;
public VotingService(PlatformConfig config, NpgsqlConnection db,
IConnectionMultiplexer redis, IProducer<Null, string> kafka)
{
_config = config; _db = db; _redis = redis; _kafka = kafka;
}
public async Task<VoteResult> CastVoteAsync(
Guid userId, CastVoteRequest request)
{
var cache = _redis.GetDatabase();
var voteKey = $"vote:{userId}:{request.TargetType}:{request.TargetId}";
var existing = await cache.StringGetAsync(voteKey);
var currentVote = existing.HasValue
? Enum.Parse<VoteDirection>(existing!) : VoteDirection.None;
if (currentVote == request.Value)
return new VoteResult(true, 0, false);
await cache.StringSetAsync(voteKey, request.Value.ToString(),
TimeSpan.FromHours(24));
int delta = (int)request.Value - (int)currentVote;
await _kafka.ProduceAsync("vote-events", new Message<Null, string>
{
Value = JsonSerializer.Serialize(new
{
EventType = "VoteChanged", UserId = userId.ToString(),
TargetId = request.TargetId.ToString(),
TargetType = request.TargetType, Delta = delta
})
});
return new VoteResult(true, delta, true);
}
public async Task BatchUpdateScoresAsync()
{
await using var postCmd = new NpgsqlCommand(@"
UPDATE posts SET upvote_count = sub.up, downvote_count = sub.down
FROM (SELECT target_id,
COUNT(*) FILTER (WHERE value = 1) AS up,
COUNT(*) FILTER (WHERE value = -1) AS down
FROM votes WHERE target_type = 'post'
GROUP BY target_id) sub
WHERE posts.id = sub.target_id", _db);
await postCmd.ExecuteNonQueryAsync();
await using var cmtCmd = new NpgsqlCommand(@"
UPDATE comments SET upvote_count = sub.up, downvote_count = sub.down
FROM (SELECT target_id,
COUNT(*) FILTER (WHERE value = 1) AS up,
COUNT(*) FILTER (WHERE value = -1) AS down
FROM votes WHERE target_type = 'comment'
GROUP BY target_id) sub
WHERE comments.id = sub.target_id", _db);
await cmtCmd.ExecuteNonQueryAsync();
}
}
public class FeedService
{
private readonly PlatformConfig _config;
private readonly NpgsqlConnection _db;
private readonly IConnectionMultiplexer _redis;
private readonly FeedRanker _ranker = new();
public FeedService(PlatformConfig config, NpgsqlConnection db,
IConnectionMultiplexer redis)
{
_config = config; _db = db; _redis = redis;
}
public async Task<FeedResult> GetFeedAsync(
string userId, SortType sortType, string? cursor, int limit = 25)
{
var cache = _redis.GetDatabase();
var cacheKey = $"feed:{userId}:{sortType}";
var cached = await cache.StringGetAsync(cacheKey);
List<FeedItem> feedItems;
if (cached.HasValue)
feedItems = JsonSerializer.Deserialize<List<FeedItem>>(cached!) ?? new();
else
{
feedItems = await BuildFeedFromDbAsync(userId, sortType);
await cache.StringSetAsync(cacheKey,
JsonSerializer.Serialize(feedItems),
TimeSpan.FromMinutes(_config.FeedCacheTtlMinutes));
}
int offset = 0;
if (!string.IsNullOrEmpty(cursor))
{
var cursorBytes = Convert.FromBase64String(cursor);
var lastId = Encoding.UTF8.GetString(cursorBytes).Split('|')[0];
offset = feedItems.FindIndex(f => f.Post.Id.ToString() == lastId) + 1;
}
var page = feedItems.Skip(offset).Take(limit).ToList();
string? nextCursor = null;
if (offset + limit < feedItems.Count)
{
var last = feedItems[offset + limit - 1];
nextCursor = Convert.ToBase64String(Encoding.UTF8.GetBytes(
$"{last.Post.Id}|{last.Post.CreatedAt.Ticks}|{last.Post.Score}"));
}
return new FeedResult(page, nextCursor, offset + limit < feedItems.Count);
}
public async Task<FeedResult> GetSubredditFeedAsync(
Guid subredditId, SortType sortType, string? cursor, int limit = 25)
{
var orderBy = sortType switch
{
SortType.New => "created_at DESC",
SortType.Top => "score DESC",
_ => "hot_score DESC"
};
await using var cmd = new NpgsqlCommand($@"
SELECT p.id, p.title, p.body, p.post_type, p.author_id,
p.subreddit_id, p.url, p.upvote_count, p.downvote_count,
p.comment_count, p.hot_score, p.created_at, u.username
FROM posts p JOIN users u ON p.author_id = u.id
WHERE p.subreddit_id = @subId
ORDER BY {orderBy} LIMIT @limit", _db);
cmd.Parameters.AddWithValue("subId", subredditId);
cmd.Parameters.AddWithValue("limit", limit + 1);
var items = new List<FeedItem>();
await using var r = await cmd.ExecuteReaderAsync();
bool hasMore = false; int count = 0;
while (await r.ReadAsync())
{
count++;
if (count > limit) { hasMore = true; break; }
var post = new Post
{
Id = r.GetGuid(0), Title = r.GetString(1),
Body = r.IsDBNull(2) ? null : r.GetString(2),
PostType = Enum.Parse<PostType>(r.GetString(3), true),
AuthorId = r.GetGuid(4), SubredditId = r.GetGuid(5),
Url = r.IsDBNull(6) ? null : r.GetString(6),
UpvoteCount = r.GetInt32(7), DownvoteCount = r.GetInt32(8),
CommentCount = r.GetInt32(9), HotScore = r.GetDouble(10),
CreatedAt = r.GetDateTime(11)
};
items.Add(new FeedItem(post, "", r.GetString(12), null, 0, null));
}
return new FeedResult(items, null, hasMore);
}
private async Task<List<FeedItem>> BuildFeedFromDbAsync(
string userId, SortType sortType)
{
var orderBy = sortType switch
{
SortType.New => "p.created_at DESC",
SortType.Top => "p.score DESC",
_ => "p.hot_score DESC"
};
await using var cmd = new NpgsqlCommand($@"
SELECT p.id, p.title, p.body, p.post_type, p.author_id,
p.subreddit_id, p.url, p.upvote_count, p.downvote_count,
p.comment_count, p.hot_score, p.created_at, p.is_nsfw, u.username
FROM posts p JOIN users u ON p.author_id = u.id
WHERE p.is_nsfw = FALSE
ORDER BY {orderBy} LIMIT 500", _db);
var items = new List<FeedItem>();
await using var r = await cmd.ExecuteReaderAsync();
while (await r.ReadAsync())
{
var post = new Post
{
Id = r.GetGuid(0), Title = r.GetString(1),
Body = r.IsDBNull(2) ? null : r.GetString(2),
PostType = Enum.Parse<PostType>(r.GetString(3), true),
AuthorId = r.GetGuid(4), SubredditId = r.GetGuid(5),
Url = r.IsDBNull(6) ? null : r.GetString(6),
UpvoteCount = r.GetInt32(7), DownvoteCount = r.GetInt32(8),
CommentCount = r.GetInt32(9), HotScore = r.GetDouble(10),
CreatedAt = r.GetDateTime(11), IsNsfw = r.GetBoolean(12)
};
items.Add(new FeedItem(post, "", r.GetString(13), null, 0, null));
}
return items;
}
}
public class ModerationService
{
private readonly PlatformConfig _config;
private readonly NpgsqlConnection _db;
private readonly IConnectionMultiplexer _redis;
private readonly IProducer<Null, string> _kafka;
public ModerationService(PlatformConfig config, NpgsqlConnection db,
IConnectionMultiplexer redis, IProducer<Null, string> kafka)
{
_config = config; _db = db; _redis = redis; _kafka = kafka;
}
public async Task<ModerationLog> ModerateAsync(
Guid moderatorId, Guid subredditId,
ModerationAction action, string? reason,
Guid? targetUserId = null, Guid? targetPostId = null)
{
var log = new ModerationLog
{
Id = Guid.NewGuid(), Action = action,
ModeratorId = moderatorId, TargetUserId = targetUserId,
TargetPostId = targetPostId, SubredditId = subredditId,
Reason = reason, CreatedAt = DateTime.UtcNow
};
await using var cmd = new NpgsqlCommand(@"
INSERT INTO moderation_logs
(id, action, moderator_id, target_user_id,
target_post_id, subreddit_id, reason, created_at)
VALUES (@id, @action, @mod_id, @user_id,
@post_id, @sub_id, @reason, @created_at)", _db);
cmd.Parameters.AddWithValue("id", log.Id);
cmd.Parameters.AddWithValue("action", log.Action.ToString());
cmd.Parameters.AddWithValue("mod_id", log.ModeratorId);
cmd.Parameters.AddWithValue("user_id",
(object?)log.TargetUserId ?? DBNull.Value);
cmd.Parameters.AddWithValue("post_id",
(object?)log.TargetPostId ?? DBNull.Value);
cmd.Parameters.AddWithValue("sub_id", log.SubredditId);
cmd.Parameters.AddWithValue("reason",
(object?)log.Reason ?? DBNull.Value);
cmd.Parameters.AddWithValue("created_at", log.CreatedAt);
await cmd.ExecuteNonQueryAsync();
if (action == ModerationAction.Remove && targetPostId.HasValue)
{
await using var del = new NpgsqlCommand(
"DELETE FROM posts WHERE id = @id", _db);
del.Parameters.AddWithValue("id", targetPostId.Value);
await del.ExecuteNonQueryAsync();
await _redis.GetDatabase().KeyDeleteAsync($"post:{targetPostId}");
}
return log;
}
public async Task<bool> IsUserBannedAsync(Guid userId, Guid subredditId)
{
await using var cmd = new NpgsqlCommand(@"
SELECT COUNT(*) FROM subreddit_bans
WHERE user_id = @userId AND subreddit_id = @subId
AND (expires_at IS NULL OR expires_at > NOW())", _db);
cmd.Parameters.AddWithValue("userId", userId);
cmd.Parameters.AddWithValue("subId", subredditId);
return (long)(await cmd.ExecuteScalarAsync() ?? 0L) > 0;
}
}
public class AwardService
{
private readonly PlatformConfig _config;
private readonly NpgsqlConnection _db;
private readonly IConnectionMultiplexer _redis;
public AwardService(PlatformConfig config, NpgsqlConnection db,
IConnectionMultiplexer redis)
{
_config = config; _db = db; _redis = redis;
}
public async Task<bool> GiveAwardAsync(
string giverId, string targetId, Guid awardId)
{
await using var tx = await _db.BeginTransactionAsync();
try
{
await using var ac = new NpgsqlCommand(@"
SELECT cost_coins, karma_bonus, coins_to_recipient
FROM awards WHERE id = @id", _db);
ac.Parameters.AddWithValue("id", awardId);
await using var ar = await ac.ExecuteReaderAsync();
if (!await ar.ReadAsync()) { await tx.RollbackAsync(); return false; }
int cost = ar.GetInt32(0), karma = ar.GetInt32(1), credit = ar.GetInt32(2);
await ar.Close();
await using var bc = new NpgsqlCommand(
"SELECT coins FROM users WHERE id = @id", _db);
bc.Parameters.AddWithValue("id", Guid.Parse(giverId));
if ((int)(await bc.ExecuteScalarAsync() ?? 0) < cost)
{ await tx.RollbackAsync(); return false; }
await using var dc = new NpgsqlCommand(
"UPDATE users SET coins = coins - @c WHERE id = @id", _db);
dc.Parameters.AddWithValue("c", cost);
dc.Parameters.AddWithValue("id", Guid.Parse(giverId));
await dc.ExecuteNonQueryAsync();
if (credit > 0)
{
await using var cc = new NpgsqlCommand(
"UPDATE users SET coins = coins + @c WHERE id = @id", _db);
cc.Parameters.AddWithValue("c", credit);
cc.Parameters.AddWithValue("id", Guid.Parse(targetId));
await cc.ExecuteNonQueryAsync();
}
if (karma > 0)
{
await using var kc = new NpgsqlCommand(
"UPDATE users SET karma_total = karma_total + @k WHERE id = @id", _db);
kc.Parameters.AddWithValue("k", karma);
kc.Parameters.AddWithValue("id", Guid.Parse(targetId));
await kc.ExecuteNonQueryAsync();
}
await tx.CommitAsync();
return true;
}
catch { await tx.RollbackAsync(); throw; }
}
}
public class RedditPlatform
{
public PostService Posts { get; }
public CommentService Comments { get; }
public VotingService Voting { get; }
public FeedService Feed { get; }
public ModerationService Moderation { get; }
public AwardService Awards { get; }
public RedditPlatform(PlatformConfig config)
{
var db = new NpgsqlConnection(config.DatabaseConnectionString);
var redis = ConnectionMultiplexer.Connect(config.RedisConnectionString);
var kafka = new ProducerBuilder<Null, string>(
new ProducerConfig { BootstrapServers = config.KafkaBootstrapServers }).Build();
Posts = new PostService(config, db, redis, kafka);
Comments = new CommentService(config, db, redis, kafka);
Voting = new VotingService(config, db, redis, kafka);
Feed = new FeedService(config, db, redis);
Moderation = new ModerationService(config, db, redis, kafka);
Awards = new AwardService(config, db, redis);
}
}
}
25. Conclusion
Designing a Reddit-scale community platform is a masterclass in distributed systems engineering. Every major subsystem — from the voting pipeline to the comment tree, from feed generation to content moderation — involves deep trade-offs between consistency, availability, latency, and cost.
Key Takeaways
- Voting is the hardest scaling challenge — it requires a dedicated ingestion pipeline with Kafka buffering and batch aggregation to handle 250K+ votes/sec.
- Comment trees benefit from LTREE — PostgreSQL ltree extension provides efficient subtree queries without recursive joins.
- Feed ranking is not just about votes — the Hot algorithm elegantly combines score magnitude, time decay, and log compression.
- Polyglot persistence is necessary — no single database can efficiently serve all access patterns at Reddit's scale.
- Cache stampede prevention matters — probabilistic early expiration and mutex locks protect against thundering herd on cache miss.
- Multi-region requires careful consistency planning — read-your-writes tokens solve the most pressing consistency needs without global locks.
- Content moderation is a layered system — from ML classifiers to AutoModerator to community reports, no single layer is sufficient.
The C# implementation demonstrates how these concepts translate into production code: post creation with Kafka events, comment trees with LTREE paths, voting with Redis write-through, and feeds with multi-level caching and cursor-based pagination.
For senior+ engineers, the key insight is that every design decision is a trade-off. Pre-computed feeds trade freshness for latency. Eventual consistency trades accuracy for availability. Database sharding trades cross-query flexibility for write scalability. The art of system design is choosing the right trade-offs for your specific constraints and scale.
Further Reading
- Reddit engineering blog on storing and serving 1.7B monthly page views
- The Architecture of Open Source Applications — Reddit chapter
- Wilson score interval — Mathematical foundation for comment sorting
- Cassandra vs PostgreSQL — When to choose which for write-heavy workloads
- Kafka patterns for event sourcing at scale
8. Comment Thread System
Reddit's nested comment system is one of its defining features. Comments form a tree where each comment can reply to any other comment, creating threads that can nest 10+ levels deep. The design must support efficient tree traversal, collapsing, and sorting.
Tree Representation Strategies
Strategy 1: Adjacency List (Parent Reference)
Each comment stores
parent_comment_id. Simple but requires recursive queries to fetch a full subtree. PostgreSQL's recursive CTEs handle this efficiently up to moderate depth.Strategy 2: Materialized Path (LTREE)
Each comment stores a path like
1.4.7.12representing the ancestor chain. Enables fast subtree queries withWHERE path <@ '1.4'. This is Reddit's actual approach in PostgreSQL.Strategy 3: Nested Set Model
Each comment has
leftandrightbounds. Subtree queries are O(1) lookups, but inserts require rebalancing. Not suitable for Reddit's write-heavy comment system.Comment Tree Fetch Query
Sort Options for Comments
Wilson Score for "Best" Sort
Where p_hat = positive / total votes, n = total votes, z = 1.96 for 95% confidence
The Wilson score interval gives us the lower bound of a 95% confidence interval for the true proportion of upvotes. This naturally favors comments with more votes and higher upvote ratios, preventing a comment with 1 upvote from outranking one with 1000 upvotes.