system-design48 min read

How to Design Community Platform like Reddit — A Senior+ Guide | Ayodhyya

How to Design Community Platform like Reddit

Building subreddits, threaded discussions, voting, and content ranking at 1.7B+ monthly visit scale

Ayodhyya July 14, 2026 ~45 min read System Design

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

MetricValueImplication
Monthly Visits1.7 Billion+~650K requests/sec peak
Daily Active Users52 Million+Massive read-heavy workload
Posts Per Day~1.5 MillionHigh write throughput for posts
Comments Per Day~16 MillionComment tree is the hot path
Active Subreddits100,000+Multi-tenant isolation needed
Votes Per Second (peak)~250,000Write-heavy vote pipeline
Average Post Size~2 KB text + media refMedia stored externally
Comment DepthAverage 4–6 levelsNested 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

RequirementTargetNotes
Availability99.99%~53 min downtime/year
Feed Latency (p99)< 200msCached feeds served from edge
Post Read Latency (p99)< 300msIncludes comment tree
Vote Latency (p99)< 100msOptimistic UI + async backend
ConsistencyEventualStrong for user auth, eventual for feeds
Durability99.999999%Multi-region replication
Throughput1M+ reads/secThrough caching layer
Write Throughput50K+ writes/secPosts + comments + votes

3. Capacity Estimation & Back-of-Envelope

Traffic Estimates

Assume 200M daily active users (DAU), each making ~5 page views per day.

Total Page Views = 200M x 5 = 1B/day = ~11,600 page views/sec

Read-heavy system: 95% reads, 5% writes.

Read QPS = 11,600 x 0.95 = ~11,000 reads/sec
Write QPS = 11,600 x 0.05 = ~580 writes/sec

Peak traffic (2x average):

Peak Read QPS = ~22,000 reads/sec
Peak Write QPS = ~1,160 writes/sec
With caching: 95% cache hit = 1,100 uncached reads/sec at peak

Storage Estimates

EntityDaily CountSize EachDaily StorageAnnual Storage
Posts1.5M2 KB3 GB~1.1 TB
Comments16M1 KB16 GB~5.8 TB
Votes200M50 B10 GB~3.6 TB
User Profiles100K new2 KB200 MB~73 GB
Media Metadata1.5M1 KB1.5 GB~548 GB
Messages5M500 B2.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

Inbound: 580 writes/sec x 2 KB = ~1.2 MB/s = ~100 Mbps
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

erDiagram USER { uuid id PK string username UK string email UK string password_hash string avatar_url int karma_post int karma_comment boolean is_premium timestamp created_at } SUBREDDIT { uuid id PK string name UK string description uuid creator_id FK string icon_url int subscriber_count timestamp created_at } POST { uuid id PK string title text body string post_type uuid author_id FK uuid subreddit_id FK int upvote_count int downvote_count int comment_count float hot_score timestamp created_at } COMMENT { uuid id PK text body uuid author_id FK uuid post_id FK uuid parent_comment_id FK int depth int upvote_count int downvote_count timestamp created_at } VOTE { uuid id PK uuid user_id FK uuid target_id FK string target_type int value timestamp created_at } USER ||--o{ POST : writes USER ||--o{ COMMENT : writes USER ||--o{ VOTE : casts SUBREDDIT ||--o{ POST : contains POST ||--o{ COMMENT : has COMMENT ||--o{ COMMENT : replies_to

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

MethodEndpointDescriptionAuth
POST/api/v1/auth/registerCreate accountNo
POST/api/v1/auth/loginLogin, return JWTNo
GET/api/v1/users/{username}Get user profileNo
PUT/api/v1/users/meUpdate own profileYes
POST/api/v1/subredditsCreate subredditYes
GET/api/v1/subreddits/{name}Get subreddit infoNo
POST/api/v1/subreddits/{name}/subscribeJoin/leaveYes
POST/api/v1/subreddits/{name}/postsCreate postYes
GET/api/v1/subreddits/{name}/postsList posts (sorted)No
GET/api/v1/posts/{id}Get post + commentsNo
POST/api/v1/posts/{id}/commentsAdd commentYes
POST/api/v1/votesCast voteYes
GET/api/v1/feed/homePersonalized home feedYes
GET/api/v1/feed/popularPopular across RedditNo
GET/api/v1/searchSearch posts/commentsNo
POST/api/v1/media/uploadUpload mediaYes
POST/api/v1/reportsReport contentYes
POST/api/v1/awards/giveGive awardYes

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

graph TB subgraph Client WEB[Web Browser] MOB[Mobile App] end subgraph EdgeLayer CDN[CDN - CloudFront] LB[Load Balancer - ALB] WAF[WAF - Rate Limiting] end subgraph APIGateway GW[API Gateway] AUTH[Auth Service] RL[Rate Limiter] end subgraph CoreServices POST_SVC[Post Service] COMMENT_SVC[Comment Service] VOTE_SVC[Voting Service] FEED_SVC[Feed Service] SEARCH_SVC[Search Service] USER_SVC[User Service] SUB_SVC[Subreddit Service] MOD_SVC[Moderation Service] MEDIA_SVC[Media Service] end subgraph DataLayer PG[(PostgreSQL)] REDIS[(Redis Cluster)] ES[(Elasticsearch)] CASS[(Cassandra)] S3[(S3 Media)] KAFKA[Kafka Bus] end subgraph Background RANKING[Ranking Worker] SPAM[Spam Detection] NOTIF[Notification Worker] AGG[Score Aggregator] end WEB --> CDN MOB --> CDN CDN --> LB LB --> WAF WAF --> GW GW --> AUTH GW --> RL GW --> POST_SVC GW --> COMMENT_SVC GW --> VOTE_SVC GW --> FEED_SVC GW --> SEARCH_SVC GW --> USER_SVC GW --> SUB_SVC GW --> MOD_SVC GW --> MEDIA_SVC POST_SVC --> PG POST_SVC --> REDIS POST_SVC --> KAFKA COMMENT_SVC --> PG COMMENT_SVC --> REDIS VOTE_SVC --> CASS VOTE_SVC --> REDIS VOTE_SVC --> KAFKA FEED_SVC --> REDIS FEED_SVC --> PG SEARCH_SVC --> ES USER_SVC --> PG USER_SVC --> REDIS SUB_SVC --> PG MEDIA_SVC --> S3 KAFKA --> RANKING KAFKA --> SPAM KAFKA --> NOTIF KAFKA --> AGG RANKING --> REDIS AGG --> PG

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.

sequenceDiagram participant Client participant APIGateway participant PostService participant ValidationService participant SpamService participant Database participant Kafka participant SearchIndex Client->>APIGateway: POST /posts APIGateway->>PostService: CreatePost PostService->>ValidationService: Validate content ValidationService-->>PostService: Valid PostService->>SpamService: Check spam score SpamService-->>PostService: Score 0.1 PostService->>Database: INSERT post PostService->>Kafka: Publish PostCreated Kafka-->>SearchIndex: Index post PostService-->>Client: 201 Created

Post Type Handling

Post TypeStorageProcessingSpecial Handling
TextPostgreSQL body columnMarkdown to HTMLAuto-save drafts
LinkURL in post tableOG metadata fetchLink preview card
ImageS3 + metadata in JSONBResize, WebP conversionNSFW classification
VideoS3 + transcode manifestHLS transcode, thumbnailsDuration limits
GalleryS3 array + metadataPer-image processingMax 20 images
PollJSONB options + votes tableExpiration timerResults reveal mode
AMAText + scheduling metadataQ&A pairing logicTime-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
        };
    }
}

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.

graph LR ROOT[Post Root] C1[Comment A] C2[Comment B] C3[Comment C] C4[Reply A.1] C5[Reply A.2] C6[Reply B.1] C7[Reply C.1] C8[Reply A.1.1] C9[Reply A.1.2] ROOT --> C1 ROOT --> C2 ROOT --> C3 C1 --> C4 C1 --> C5 C2 --> C6 C3 --> C7 C4 --> C8 C4 --> C9

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.12 representing the ancestor chain. Enables fast subtree queries with WHERE path <@ '1.4'. This is Reddit's actual approach in PostgreSQL.

Strategy 3: Nested Set Model

Each comment has left and right bounds. Subtree queries are O(1) lookups, but inserts require rebalancing. Not suitable for Reddit's write-heavy comment system.

Comment Tree Fetch Query

WITH RECURSIVE comment_tree AS (
    SELECT c.id, c.body, c.author_id, c.parent_comment_id,
        c.depth, c.path, c.upvote_count, c.downvote_count,
        c.upvote_count - c.downvote_count AS score,
        c.created_at, u.username AS author_name,
        ARRAY[c.path] AS sort_path
    FROM comments c
    JOIN users u ON c.author_id = u.id
    WHERE c.post_id = @postId
      AND c.parent_comment_id IS NULL
      AND c.is_deleted = FALSE

    UNION ALL

    SELECT c.id, c.body, c.author_id, c.parent_comment_id,
        c.depth, c.path, c.upvote_count, c.downvote_count,
        c.upvote_count - c.downvote_count AS score,
        c.created_at, u.username AS author_name,
        ct.sort_path || c.path
    FROM comments c
    JOIN users u ON c.author_id = u.id
    JOIN comment_tree ct ON c.parent_comment_id = ct.id
    WHERE c.is_deleted = FALSE AND c.depth <= 10
)
SELECT * FROM comment_tree ORDER BY sort_path;

Sort Options for Comments

Sort TypeAlgorithmUse Case
BestWilson score confidence intervalDefault — balances score + vote count
Topupvotes - downvotes (raw score)Most upvoted
Newcreated_at DESCLatest first
Oldcreated_at ASCOldest first
ControversialControversy score formulaHotly debated
Q&AOP replies highlightedAMA threads

Wilson Score for "Best" Sort

Wilson Score = (p_hat + z^2/2n +/- z*sqrt(p_hat*(1-p_hat)/n + z^2/4n^2)) / (1 + z^2/n)
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.

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.

sequenceDiagram participant Client participant VoteService participant Redis participant Kafka participant Aggregator participant Database Client->>VoteService: POST /votes {target_id, value: 1} VoteService->>Redis: GET vote:{user_id}:{target_id} alt New vote or changed VoteService->>Redis: SET vote:{user_id}:{target_id} = 1 VoteService->>Kafka: Publish VoteChanged VoteService-->>Client: 200 OK Kafka->>Aggregator: Consume VoteChanged Aggregator->>Database: UPSERT vote + UPDATE score Aggregator->>Redis: UPDATE cached score end

Vote Ingestion Pipeline

At peak, Reddit processes 250,000+ votes per second. Writing each vote directly to PostgreSQL would overwhelm the database. Instead:

  1. 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.
  2. Kafka Buffer: A VoteChanged event is published to Kafka, decoupling the user-facing response from database writes.
  3. Batch Aggregator: A consumer groups votes by target_id and batch-updates PostgreSQL every 5 seconds.
  4. 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

Hot Score = (log10(max(|votes|, 1)) x sign(votes)) + (created_at_epoch / 45000)

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

AlgorithmPrimary SignalTime WeightBest For
HotScore magnitude + timeLog decay, 12.5h half-lifeDefault home feed
NewCreated time onlyNone (pure time sort)Discovering fresh content
TopNet upvotesWindow-based filterBest content of time period
RisingVote velocity12h exponential decayContent gaining momentum
ControversialVote ratio balanceMagnitude weightingDebate/discussion threads
BestWilson lower boundNoneComment 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

graph TD A[Reddit Admins - Site-wide] --> B[Subreddit Creator] B --> C[Head Moderator] C --> D[Moderator] C --> E[AutoModerator Bot] D --> F[Approved Submitter] E --> G[Automated Rules Engine] style A fill:#ef4444,color:#fff style B fill:#f59e0b,color:#fff style C fill:#0088ff,color:#fff style D fill:#10b981,color:#fff style E fill:#8b5cf6,color:#fff

AutoModerator Rules Engine

Rule TypeMatch CriteriaActions
Keyword FilterTitle/body contains word/regexRemove, report, flair
Domain BlockURL domain in blocklistRemove + notify
Account AgeAccount less than X days oldRemove, queue for review
Karma ThresholdComment karma less than thresholdRemove, message user
Repost DetectionSimilar title within 30 daysRemove, suggest original
Report ThresholdReports greater than N within M minutesAuto-remove, alert mods
Flair RequirementPost missing required flairRemove 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 };
    }
}

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.

graph TB subgraph FeedTypes HOME[Home Feed] POPULAR[Popular Feed] SUB[Subreddit Feed] end subgraph CacheLayer WARM[Warm Cache - Pre-computed] HOT[Hot Cache - Redis] MISS[Cache Miss - DB Query] end subgraph Pipeline RANK[Apply Personalized Ranking] FILTER[Filter Blocked NSFW] PAGINATE[Paginate Results] end HOME --> WARM POPULAR --> WARM SUB --> HOT WARM -->|Miss| HOT HOT -->|Miss| MISS MISS --> RANK RANK --> FILTER FILTER --> PAGINATE PAGINATE --> WARM

Feed Types and Their Generation Strategy

Feed TypeGenerationCache TTLPersonalization
HomePre-computed per user5 minSubscriptions + karma-weighted
PopularGlobal hot posts (non-NSFW)2 minGeo-weighted
SubredditHot/New/Top per subreddit1 minSubreddit-specific
AllEverything (admin only)5 minNone
RisingVelocity-based global5 minNone

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.

sequenceDiagram participant Browser participant WebSocketGateway participant PubSub participant CommentService Browser->>WebSocketGateway: Connect (auth token) WebSocketGateway->>PubSub: Subscribe to user channels Note over Browser,CommentService: User creates a comment CommentService->>PubSub: Publish CommentCreated PubSub->>WebSocketGateway: Deliver to post subscribers WebSocketGateway->>Browser: New comment notification Browser->>Browser: Append comment to thread

Event Channels

ChannelTriggerAudienceFrequency
post:{id}:commentsNew comment on postUsers viewing that postHigh
user:{id}:notificationsReply, mention, messageSpecific userMedium
subreddit:{id}:newNew post in subredditUsers browsing subredditMedium
post:{id}:votesScore changeUsers viewing postVery High
livethread:{id}Live thread updateSubscribed usersBurst
chat:{subreddit}Chat messageChat participantsMedium

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

AwardCoins CostKarma BonusPremium DurationCoins to Recipient
Silver100+10None0
Gold500+1001 week100
Platinum1800+7001 month700
Community AwardVariable+VariableNoneVariable
Mod AwardFree (mod only)+251 week0

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.

graph LR A[Client Upload] --> B[Pre-Signed URL] B --> C[S3 Upload] C --> D[Virus Scanner] D --> E[Content Classifier] E --> F{Media Type} F -->|Image| G[Resize + WebP] F -->|Video| H[HLS Transcode] F -->|GIF| I[Optimize] G --> J[CDN Distribution] H --> J I --> J J --> K[Update Metadata DB]

Media Constraints

TypeMax SizeFormatsProcessingMax Duration
Image20 MBJPG, PNG, GIF, WebPResize to 3 sizesN/A
Video1 GBMP4, MOV, WebMHLS transcode (360p/720p/1080p)15 min
GIF100 MBGIFConvert to MP4 + keep GIF60 sec
Gallery20 imagesAny image formatPer-image processingN/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.

graph TB subgraph Features A[User Features] B[Post Features] C[Interaction Features] end subgraph Models D[Collaborative Filtering] E[Content-Based Filtering] F[Deep Learning Ranker] end subgraph Outputs G[Home Feed Re-ranking] H[Subreddit Suggestions] I[Similar Posts] end A --> D A --> F B --> E B --> F C --> D C --> F D --> G E --> H E --> I F --> G

Feature Engineering

FeatureSourceUpdate Frequency
Subreddit subscription setUser profileReal-time
Voting history by categoryVotes tableDaily batch
Comment activityComments tableDaily batch
Time-of-day activity patternActivity logsWeekly batch
Content embedding (BERT)Post titles/bodiesOn publish
Author following graphSocial graphReal-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;
    }
}

19. Database Sharding Strategy

At Reddit's scale, a single PostgreSQL instance cannot hold all data. We shard across multiple dimensions depending on the entity type.

Sharding by Entity Type

EntityShard KeyShard CountRationale
Usersuser_id hash256Even distribution, reads by user_id
Subredditssubreddit_id hash64~100K subreddits, moderate size
Postssubreddit_id256Co-locate posts in same subreddit
Commentspost_id256Co-locate with parent post
Votestarget_id hash512Highest write volume
Messagesconversation_id128Co-locate participants

Why Shard Posts by Subreddit?

Sharding posts by subreddit_id means all posts for a subreddit live on the same shard. This enables efficient range queries like "get all posts in r/programming sorted by hot" without cross-shard scatter-gather. The trade-off is potential hotspots for very active subreddits like r/AskReddit, which we handle with virtual sharding (splitting hot subreddits across multiple virtual shards).

20. Caching Strategy

Multi-Level Cache Architecture

graph TB A[Client Request] --> B[L1 - CDN Cache] B -->|Miss| C[L2 - App Local Cache] C -->|Miss| D[L3 - Redis Cluster] D -->|Miss| E[L4 - Database] B -->|Static| F[CloudFront Edge] C -->|Hot posts| G[In-Memory LRU] D -->|Feed + Scores| H[Redis 500+ nodes] E -->|Source of truth| I[PostgreSQL + Cassandra]

Cache Keys and TTLs

Cache Key PatternStoreTTLInvalidation
post:{id}Redis15 minOn vote/comment update
feed:{user_id}:homeRedis5 minOn new post in sub
feed:popular:hotRedis2 minPeriodic refresh
subreddit:{name}:postsRedis1 minOn new post
score:post:{id}Redis5 minOn vote change
vote:{user_id}:{target_id}Redis24 hoursOn vote change
user:{id}:profileRedis30 minOn profile update
search:autocomplete:{prefix}Redis5 minPeriodic 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

graph TB subgraph US_EAST[US East - Primary] LB1[Load Balancer] APP1[App Cluster] DB1[(PostgreSQL Primary)] REDIS1[(Redis Primary)] CASS1[(Cassandra US)] end subgraph EU_WEST[EU West - Secondary] LB2[Load Balancer] APP2[App Cluster] DB2[(PostgreSQL Replica)] REDIS2[(Redis Replica)] CASS2[(Cassandra EU)] end subgraph AP_SOUTH[AP South - Tertiary] LB3[Load Balancer] APP3[App Cluster] DB3[(PostgreSQL Replica)] REDIS3[(Redis Replica)] CASS3[(Cassandra AP)] end DB1 -->|Async Replication| DB2 DB1 -->|Async Replication| DB3 REDIS1 -->|CRDT Sync| REDIS2 REDIS1 -->|CRDT Sync| REDIS3 CASS1 -->|Multi-DC Replication| CASS2 CASS1 -->|Multi-DC Replication| CASS3 DNS[Route53 GeoDNS] --> LB1 DNS --> LB2 DNS --> LB3

Data Replication Strategy

Data TypeReplicationConsistencyConflict Resolution
User profilesAsync PostgreSQL streamingEventual (max 5s lag)Last-write-wins
PostsAsync PostgreSQL streamingEventual (max 5s lag)Origin-region wins
CommentsAsync PostgreSQL streamingEventual (max 5s lag)Origin-region wins
VotesCassandra multi-DCEventual (strong within DC)LWW (Cassandra default)
Feed cacheRedis CRDTEventual (max 30s)CRDT merge
Search indexElasticsearch CCREventual (max 30s)N/A (read-only replicas)

22. Cost Estimation

Infrastructure Cost Breakdown (Monthly)

ComponentSpecificationMonthly 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
Elasticsearch50 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 CDN5PB/month transfer$400,000
ALB + WAF10 ALBs + WAF rules$25,000
Route 53 + SSLDNS + certificates$5,000
Background Workers100 x c6i.xlarge$70,000
Monitoring (CloudWatch)Full observability stack$50,000
ML InfrastructureInference + 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

© 2026 Ayodhyya. All rights reserved.

System design article for educational purposes. Not affiliated with Reddit, Inc.