System Design Problem

Design a Threaded Discussion Forum (Reddit)

Commonly Asked By:RedditMetaGooglePinterest

Interview Setup

Interview Prompt

Design a threaded discussion forum like Reddit with subreddits, nested comment trees, voting, and ranked feeds.

Clarifying Questions (ask before designing)

QuestionWhy it matters
How deep can comment threads go, and do we load the full tree or lazy-load branches?A viral post with 10K+ comments cannot return in a single query. Materialized path indexing combined with cursor pagination provides predictable retrieval, whereas recursive adjacency-list common table expressions break down under deep trees.
Which ranking modes matter most between hot, best, and controversial, or do we support all three using distinct mathematical formulas?Hot ranking applies a time-decayed logarithmic score to posts, whereas the best sort uses Wilson score confidence intervals for comments. New ranking is strictly chronological by creation timestamp. Each mode requires a dedicated Redis sorted set key and an asynchronous background recomputation path.
How stale can vote counts and karma be before users notice?A throughput of 500M votes per day (averaging ~6,000 per second with 30,000 peak) cannot hit PostgreSQL synchronously. While eventual consistency on displayed counters is acceptable, duplicate voting by the same user must be strictly prevented via atomic Redis CAS transitions backed by durable database unique constraints.
What scale are we designing for in terms of daily active users, posts, comments, and vote volume?Anchors the math: 50M DAU, 5M posts/day, 50M comments/day, 500M votes/day (555M total writes/day). At a 100:1 read-to-write ratio, this generates 55.5B reads/day (~642K QPS avg, ~1.5M peak). Storage grows at 35 GB/day raw content before indexing overhead.

Scope

In scope

  • Nested comment trees (materialized path)
  • Hot, best, and controversial ranking
  • Subreddit isolation and ranked feeds
  • Async vote aggregation (Redis + Kafka)
  • Capacity estimation with shown math

Out of scope (state explicitly)

  • Full ML ranking model training pipeline
  • Direct messaging and real-time chat infrastructure
  • Ad insertion and monetization

Functional Requirements

Core discussion capabilities include community creation, rich content publishing, deeply nested comment conversations, voting, and personalized feed generation:

  • Subreddits: Create, discover, and join topic-based interest communities.
  • Posts: Submit text posts, links, images, and videos with community-specific flair tags.
  • Comments: Engage in deeply nested, threaded discussions represented as a conversation tree.
  • Voting: Cast upvotes and downvotes on posts and comments to signal relevance and quality.
  • Ranking: View community feeds sorted by Hot, New, Top, Rising, and Controversial.
  • User Karma: Accumulate an aggregate reputation score derived from upvotes and downvotes received.
  • Home Feed: Consume an aggregated, personalized feed assembling top posts from subscribed subreddits.
  • Search: Discover relevant posts, comments, and subreddits through full-text search.
  • Moderation: Subreddit moderators manage content by removing policy-violating items and banning repeat offenders.
  • Awards and Gilding: Reward standout posts and comments with premium community awards.

Non-Functional Requirements

Comment tree reads and high-volume vote aggregation present significantly tougher scalability challenges than post creation. Your interviewer will ask which ranking mode (hot, new, or top) you optimize for first, because each implies different precomputation intervals and cache key structures.

  • High Availability: Maintain 99.99% uptime for core browsing and voting interactions.
  • Low Latency: Front page loads in under 200 ms p99, while comment threads load in under 500 ms p99.
  • Scalability: Support 50M+ daily active users and millions of new posts and comments daily.
  • Read-Heavy Workload: Accommodate a 100:1 read-to-write traffic ratio across feeds and discussion threads (555M writes/day, 55.5B reads/day, ~642,360 avg read QPS, ~1.5M peak QPS).
  • Eventual Consistency: Allow displayed vote totals and karma scores to remain slightly stale while strictly preventing duplicate voting via atomic Redis CAS transitions and authoritative database unique keys.
  • Deep Comment Trees: Gracefully navigate viral threads containing over 10,000 comments nested more than 20 levels deep.

Capacity Estimations

Evaluating write volume, read-to-write distribution, and vote velocity indicates whether ranking scores should be computed synchronously on read or precalculated in background queues:

MetricCalculationValue
DAUGiven (product assumption)50M
Posts / day50M DAU x 0.15M
Comments / day50M DAU x 150M
Votes / day50M DAU x 10500M
Total write volume5M posts + 50M comments + 500M votes555M/day (~6,424/sec avg, 35K peak)
Read-to-write ratioGiven (read-heavy browsing workload)100:1
Total read volume555M writes/day x 10055.5B/day (~642,360 QPS avg, ~1.5M peak)
Read QPS breakdownFeed reads ~40% (~257K QPS), threads ~50% (~321K QPS), other ~10% (~64K QPS)~642K avg QPS
Avg post sizeGiven (typical workload assumption)2 KB
Avg comment sizeGiven (typical workload assumption)500 bytes
Content storage / day(5M x 2KB) + (50M x 500B)35 GB
Hot post cache100K posts x 10 KB1 GB
Active home-feed mergesBounded to active window: ~2.5M concurrent users ÷ 300s TTL (lazy rebuild for inactive)~8,300 merges/sec

Read Workload & Feed Merge Sizing: At 50M DAU, total write volume across posts (5M), comments (50M), and votes (500M) reaches 555M writes/day (~6,424 writes/sec average, 35,000 peak). With a 100:1 read-to-write browsing ratio, total read traffic reaches 55.5 Billion reads/day (~642,360 average read QPS, peaking at ~1.5M QPS). Feed reads account for ~40% (~257K QPS), comment thread loads account for ~50% (~321K QPS), and search/user reputation lookups represent ~10% (~64K QPS). Over 95% of feed and score reads hit Redis and CDN edge tiers directly. Rebuilding home feeds for all 50M users every 5 minutes would require an impossible 166,667 merges/sec. Instead, proactive merges are strictly bounded to the active user window (~2.5M concurrent users in a 15-minute window), requiring only ~8,300 merges/sec across horizontally sharded feed workers, while inactive users are rebuilt lazily on demand.

Architecture Diagram

In an interview, clarify hot, new, and top ranking upfront because each sort mode requires its own precomputed Redis sorted set. New ranking is strictly chronological by creation timestamp and does not require worker recomputation.

The architecture navigates three critical paths: post creation (validating metadata, storing media in S3, persisting to PostgreSQL sharded by subreddit_id, and emitting post-events to Kafka), vote ingestion (atomic Redis Lua CAS state transition, publishing to Kafka with acks=all, and asynchronous batch persistence to PostgreSQL user_votes), and feed serving (reading precomputed hot scores from Redis sorted sets per subreddit, bounded active home-feed merges, and lazy-loading nested comment trees on demand).

Comments form a hierarchical tree modeled with materialized paths sharded by post_id, avoiding the trap of loading deep 10,000-comment threads in a single query. Hot ranking evaluates time-decayed logarithmic scores updated on vote events rather than on every user read. Meanwhile, the home feed aggregates content from subscribed subreddits by fetching the top 25 hot posts from each community and executing a bounded k-way merge ordered by normalized score.

Loading...

Component Deep Dives

Threaded comments and voting represent the core architectural challenges of the system. We explore ranking algorithms and cache key designs first, followed by comment tree storage and asynchronous vote aggregation.

Post Service

Handles post authoring, validation, media ingestion, and event broadcasting:

  • Create post: Validates content length and mandatory subreddit flair, uploads media attachments to S3, persists the record to PostgreSQL (sharded by subreddit_id), and publishes a post-events message to Kafka.
  • Post types: Text, link, image, video, and poll submissions, each stored with type-specific metadata schemas.
  • Flair: Enforces per-subreddit tag taxonomies such as Discussion, Question, or News.
  • Cross-posting: Creates an independent post record in the target subreddit referencing the original through crosspost_parent_id. Vote scores, comments, and community moderation remain fully isolated.
  • Awards & gilding: Handles community awards via giveAward, validating user coin balance, recording the transaction in the awards table, incrementing posts.award_count, updating Redis awards hash caches, and publishing an award-events message to Kafka.
  • Soft delete & removal: Preserves tombstone records to maintain comment tree referential integrity, replacing the author name and content body with "[deleted]". Emits a post-events removal message that triggers eviction from Redis sorted sets (ZREM), invalidation of cached home feeds, purging of CDN edge cache tags, and de-indexing from Elasticsearch.

Subreddit Service

Manages community lifecycles, subscriber memberships, access policies, and moderation tooling:

  • Community lifecycle: Oversees subreddit creation, customizable community rules, moderator assignments, and user ban enforcement.
  • Membership tracking: Maintains the user_subreddits relationship table, which the Feed Service queries to assemble personalized home feeds.
  • Access control: Supports public browsing, restricted posting where only approved contributors publish, and private invite-only subreddits.
  • Moderator actions: Provides interfaces to remove policy-violating posts and comments, ban bad actors, pin announcements, and configure AutoModerator regex filters.

Comment Service

Orchestrates comment lifecycle operations, tree indexing, and branch retrieval:

  • Materialized path storage: Encodes lineage hierarchy using dotted string paths formatted with fixed-width 6-character zero-padded base-36 segments such as 000001.000003.000007 where depth is directly derived from string segment count.
  • Concurrent sibling allocation: Allocates the next monotonic sibling segment under the parent using an atomic per-parent sequence (such as Redis counter seq:comment:{parent_id} or PostgreSQL atomic counter UPDATE comments SET child_seq = child_seq + 1 WHERE comment_id = ? RETURNING child_seq). For root comments, it allocates from seq:post:{post_id}. Sibling segments are formatted as fixed-width 6-character zero-padded base-36 identifiers (such as 000001, 000002, 00000Z), which support over 2.17B ($36^6$) siblings per parent while preserving strict ASCII lexicographical sort order. Appending this segment to the parent's path (parent.path + '.' + allocated_segment) ensures concurrent replies to the same parent receive distinct non-colliding paths without race conditions.
  • Sorting modes & ranking consistency: Supports best sorting using materialized wilson_score, top sorting by net score, new sorting by timestamp, and controversial sorting using materialized controversial_score. Top-level comments (depth = 0) are queried using idx_post_wilson (ORDER BY wilson_score DESC) or idx_post_controversial (ORDER BY controversial_score DESC), while child branches are fetched by path prefix (WHERE post_id = ? AND path LIKE '000001.000003.%'). Within each branch, child comments are ordered by path for natural pre-order conversation tree rendering in the UI. A single SQL query with ORDER BY path, score does not produce a globally Wilson-sorted tree because child nodes must remain structurally grouped under their respective parents.
  • Lazy loading: Returns top-level comments first and fetches nested reply subtrees on demand using cursor pagination on the path prefix.
  • Awards & gilding: Bestows awards on comment nodes via giveAward, persisting to the awards table, incrementing comments.award_count, and dispatching an award-events message to Kafka.
  • Soft delete: When a comment is removed, if it has child replies, its content is masked to "[deleted]" and is_removed = true to preserve path continuity for descendant replies, whereas a leaf node with no replies can simply be pruned.

Comment Tree Storage: Architectural Approaches

Modeling deeply nested comment trees requires balancing write latency against complex hierarchical reads:

Approach 1: Adjacency List (Simple Writes, Slow Deep Traversal)

SQL
-- Schema: comments (comment_id, post_id, parent_comment_id, ...)
-- Retrieving the full tree requires recursive CTEs or iterative round trips

Approach 2: Materialized Path (Recommended)

SQL
-- Schema: comments (comment_id, post_id, path, child_seq, wilson_score, controversial_score, ...)
-- path = "000001.000003.000007.00000F" (ordered lineage from root in 6-character base-36 segments)
-- Retrieve subtree: WHERE post_id = ? AND path LIKE '000001.000003.%'
-- Sorting by path yields pre-order traversal for natural UI rendering

Approach 3: Nested Set Model (Fast Reads, Heavy Write Amplification)

SQL
-- Schema: comments (comment_id, post_id, lft, rgt, ...)
-- Retrieve all descendants: WHERE lft > parent.lft AND rgt < parent.rgt
-- Inserting a comment requires locking and updating lft and rgt boundaries across the table

Approach 4: Closure Table (Flexible Queries, O(Depth) Insert Cost)

SQL
-- Schema: comment_tree (ancestor_id, descendant_id, depth)
-- Stores one row for every ancestor-descendant pairing
-- Enables fast traversal but incurs O(depth) record inserts for every reply

Recommendation: Adopt the Materialized Path pattern for a Reddit-scale system. It delivers efficient single-query subtree reads, simple append writes, and natural hierarchical sorting without recursive query overhead.

Vote Service

Processes high-throughput vote traffic while preventing write contention on relational rows:

  • Throughput challenge: Ingesting 500M votes per day generates intense write contention (~6,000 votes/sec average, 30,000 peak) when thousands of users interact with the same viral post simultaneously.
  • Atomic Lua CAS transition: Concurrent requests from the same user cannot both pass the Redis check. The Vote Service executes an atomic Lua script against user_votes:{user_id}:{entity_type}:{entity_id}:
    • Same direction: Returns an idempotent no-op without mutating state or re-emitting events.
    • New vote (was 0 or nil): Atomically sets the new direction (1 or -1) with a 30-day sliding TTL and generates net delta +1 or -1.
    • Changed vote (direction flip): Atomically replaces the old direction with the new direction and calculates net delta (+1 to -1 is -2, while -1 to +1 is +2).
    • Removed vote (direction 0): Atomically deletes the key and calculates net delta (+1 to 0 is -1, while -1 to 0 is +1).
  • Durable source of truth & TTL fallback: Redis user-vote state acts as a fast serving and deduplication cache. PostgreSQL user_votes is the durable source of truth. If a Redis vote-state key is missing or expired, the service queries PostgreSQL first before applying the transition to prevent duplicate votes across TTL boundaries. The composite primary key (user_id, entity_type, entity_id) in PostgreSQL serves as the immutable authoritative backstop.
  • Redis + Kafka failure semantics:
    1. The service applies the atomic CAS state change tentatively in Redis.
    2. It publishes an event payload to Kafka vote-events with acks=all: { event_id, user_id, entity_type, entity_id, old_direction, new_direction, delta, timestamp }.
    3. Kafka producer timeouts are treated as ambiguous outcomes, where event_id-based consumer idempotency makes replay or duplicate publication safe. The service reconciles Redis serving state with the durable event stream rather than assuming a timeout means the event was never committed. On definite pre-publish errors (such as local payload validation failure or client-side connection drops before dispatch), tentative Redis state is rolled back immediately and an HTTP 503 error is returned.
    4. On publication acknowledgement, the service immediately returns HTTP 200 with { accepted: true, voteState: new_direction, eventId, approximateScore }. Authoritative score updates remain strictly asynchronous.
  • Downstream consumer processing & delta idempotency:
    • vote-db-writer: Transactionally deduplicates incoming events by event_id against the durable PostgreSQL processed_events table (enforced by PRIMARY KEY (consumer_group, event_id)) prior to upserting user_votes and executing UPDATE posts/comments SET score = score + delta. This ensures Kafka replays or consumer retries across the full 7-day retention window never apply duplicate score deltas.
    • vote-counter: Enforces fast-path event idempotency in Redis using an atomic check-and-set with a 7-day TTL matching Kafka topic retention (SET processed_events:counter:{event_id} 1 EX 604800 NX), backed by the durable PostgreSQL processed_events ledger on cache miss. If the event has already been recorded, counter application is skipped. Otherwise, it applies net delta increments to Redis score hashes at score:{entity_type}:{entity_id} via HINCRBY. The Redis counter cache is explicitly non-authoritative and continuously reconciled against PostgreSQL durable scores.
  • Cache reconstruction: If Redis is lost, vote counts and user vote states are reconstructed from PostgreSQL user_votes or replayed from compacted Kafka event logs.

Ranking Algorithms: Deep Dive

Reddit relies on distinct scoring algorithms tailored to content types and freshness requirements:

Hot Ranking (Time-Decayed Post Scoring)

PYTHON
import math
from datetime import datetime

def hot_score(ups, downs, created_at):
    score = ups - downs
    order = math.log10(max(abs(score), 1))
    sign = 1 if score > 0 else -1 if score < 0 else 0

    # Epoch: Dec 8, 2005 (Reddit's birthday)
    epoch = datetime(2005, 12, 8, 7, 46, 43)
    seconds = (created_at - epoch).total_seconds()

    return round(sign * order + seconds / 45000, 7)
  • Time dominance: Time is the primary ranking factor. A post published 12.5 hours earlier requires 10 times more net upvotes to maintain parity with a brand-new post.
  • Logarithmic scale: Moving from 10 to 100 net votes contributes the exact same score increment as moving from 100 to 1,000 net votes, preventing popular threads from permanently dominating the front page.

Wilson Score ("Best" Comment Ranking)

PYTHON
import math

def wilson_score(ups, downs, confidence=0.95):
    n = ups + downs
    if n == 0:
        return 0
    z = 1.96  # 95% confidence
    p = ups / n

    return (p + z*z/(2*n) - z * math.sqrt((p*(1-p) + z*z/(4*n)) / n)) / (1 + z*z/n)
  • Confidence interval lower bound: Solves the imbalance where a comment with a single upvote (100% positive) would otherwise rank above a proven comment with 100 upvotes and 1 downvote (99% positive).
  • Sample size weighting: Calculates the lower bound of the 95% confidence interval for a Bernoulli parameter, stored in the materialized wilson_score column so that comments with larger sample sizes rank higher when their positive ratio is solid.

Controversial Ranking

PYTHON
def controversial_score(ups, downs):
    if ups <= 0 or downs <= 0:
        return 0
    magnitude = ups + downs
    balance = min(ups, downs) / max(ups, downs)
    return magnitude * balance

Evaluates total vote volume balanced against the ratio of minority to majority votes ((ups + downs) * (min(ups, downs) / max(ups, downs))), surfacing discussions with intense engagement and an even upvote/downvote split. For comments, this metric is materialized in the comments.controversial_score column and indexed with idx_post_controversial for efficient depth-0 subtree queries.

Rising Ranking (Velocity of Fresh Posts)

PYTHON
def rising_score(delta_ups_1h, delta_comments_1h, age_in_hours):
    # Evaluates short-term engagement velocity for posts created in the last 6 to 12 hours
    if age_in_hours > 12:
        return 0.0
    velocity = (delta_ups_1h * 2.0) + (delta_comments_1h * 3.0)
    time_decay = (age_in_hours + 0.5) ** 1.5
    return velocity / time_decay

Measures short-term engagement acceleration for posts submitted within the last 6 to 12 hours. Unlike Hot ranking (which applies logarithmic scaling across cumulative votes over a 12.5-hour half-life) or Top ranking (which tallies raw vote totals over fixed windows), Rising surfaces breakout discussions before they accumulate large aggregate totals. The Ranking Worker computes rising scores every 60 to 120 seconds and maintains the feed:sub:{subreddit_id}:rising sorted set in Redis.

New Ranking (Strict Chronological Ordering)

New ranking is strictly timestamp-based (using created_at epoch milliseconds or Snowflake ID timestamps). It is written once during post creation into feed:sub:{subreddit_id}:new and does not require continuous score recalculation by background ranking workers.

Ranking Worker

Asynchronously consumes event streams and recalculates ranking metrics without blocking the vote ingestion pipeline:

  • Event stream consumption: Subscribes to Kafka vote-events and post-events topics across dedicated consumer groups.
  • Score recalculation: Periodically recomputes logarithmic hot scores, short-term rising velocity, comment Wilson scores, and controversial scores. New sorting is strictly timestamp-based and bypassed by the scoring worker.
  • Sorted set publication: Updates Redis sorted sets including feed:sub:{subreddit_id}:hot, feed:sub:{subreddit_id}:rising, feed:sub:{subreddit_id}:top, and feed:sub:{subreddit_id}:controversial.
  • Asynchronous justification: A viral post receiving 30,000 votes per second cannot execute database row updates synchronously, meaning that offloading recalculation to background workers guarantees consistent API latencies.

Feed Service

Assembles personalized feeds by aggregating content across communities a user follows while strictly bounding compute overhead at 50M DAU:

  • Active user bounding: Rather than refreshing all 50M users every 5 minutes (which would require 166,667 merges/sec), background merges are bounded strictly to the active user window (~2.5M concurrent users active within a 15-minute sliding window), requiring only ~8,300 merges/sec across horizontally sharded feed workers.
  • Lazy recomputation for inactive users: Inactive users are not refreshed proactively, so their feeds are recomputed lazily on demand when they open the application.
  • Bounded candidate pool: The merge job evaluates the user's top 50 subscribed subreddits, fetching the top 25 hot posts from each community's Redis sorted set (capping candidate IDs at 1,250). It interleaves posts by normalized hot score rather than naive alternation.
  • Deduplication & caching: Deduplicates cross-posts using crosspost_parent_id and caches the assembled feed at feed:home:{user_id} with a 300-second TTL.
  • Stale-while-revalidate: If a cached feed expires during a user request, the stale feed is served immediately while an asynchronous task regenerates the fresh feed in the background.
  • Membership changes: Joining or leaving a subreddit invalidates or marks the user's home feed cache dirty, prompting a refreshed merge on the next view.

Search Service (Elasticsearch)

Powers full-text exploration across forum posts, comments, and community directories:

  • Multi-index architecture & shard routing: Segregates documents across posts_index, comments_index, and subreddits_index. Posts are routed by subreddit_id to isolate community-scoped queries to a single shard, comments are routed by post_id to co-locate discussion trees, and subreddits use edge-ngram analyzers for instant prefix autocomplete.
  • Query capabilities & ranking: Executes BM25 full-text scoring with title boosts, faceted filtering by community, author, post type, and creation time ranges, supporting sorting by relevance, recency (new), or net engagement (top).
  • Asynchronous pipeline (search-indexer): A dedicated Kafka consumer group consumes post-events, comment-events, and subreddit metadata to update Elasticsearch indices in near real time. Content removals and soft deletions immediately mark documents unavailable or purge them from search results.

Moderation Pipeline

Processes content events asynchronously through automated inspection models and community review workflows:

  • Spam detection model: Evaluates incoming posts for link farming, automated bot phrase repetition, and abnormal new-account posting velocity.
  • Toxicity classification: Flags abusive or harassing content for immediate human review or automated quarantine.
  • AutoModerator engine: Evaluates custom per-subreddit regular expressions and karma thresholds, automating actions such as filtering forbidden keywords or enforcing minimum account age.
  • Moderation queue: Aggregates user-submitted flags into a prioritized triage queue for community volunteer moderators.
  • Automated enforcement & propagation: When content is removed by moderators or automated classifiers, a removal event is published. Downstream consumers immediately remove the item from Redis ranked sorted sets (ZREM), invalidate active home feeds, purge CDN cache tags, and de-index the item in Elasticsearch.

Event Bus Design (Kafka)

The event bus decouples write-path producers from background consumers, absorbing traffic spikes seamlessly:

YAML
topics:
  post-events:
    description: "New post publications, content edits, soft deletions, and removals"
  comment-events:
    description: "New comment submissions, nested replies, edits, and deletions"
  vote-events:
    partitions: 128
    partition_key: "entity_id (post_id or comment_id to preserve per-entity vote ordering)"
    retention: "7 days"
    replication_factor: 3
    min_insync_replicas: 2
  award-events:
    partitions: 32
    partition_key: "entity_id (post_id or comment_id)"
    description: "Community awards and gilding carrying award_id, entity_type, entity_id, giver_user_id, award_type, and coin_cost"

producer_configuration:
  acks: "all"
  idempotence: true
  payload_schema:
    event_id: "uuid"
    user_id: "int64"
    entity_type: "post | comment"
    entity_id: "int64"
    old_direction: "1 | -1 | 0"
    new_direction: "1 | -1 | 0"
    delta: "int16"
    timestamp: "int64"

consumer_groups:
  vote-db-writer:
    purpose: "Persist durable vote records to PostgreSQL source of truth (atomically checks processed_events table before upserting user_votes and applying score deltas)"
  vote-counter:
    purpose: "Apply atomic counter updates to Redis score hashes (score:post:{id}, score:comment:{id}) deduplicating event_id via Redis SET processed_events:counter:{event_id} 1 EX 604800 NX (7 days matching Kafka retention) with PostgreSQL processed_events ledger fallback"
  ranking-worker:
    purpose: "Recompute hot, top, rising, and controversial scores for Redis feed sorted sets (new sort uses created_at directly)"
  search-indexer:
    purpose: "Consume post-events, comment-events, and community metadata to update Elasticsearch indices near real time"
  award-processor:
    purpose: "Persist award records to PostgreSQL awards table, increment entity award_count, update Redis awards hash, and attribute karma"
  moderation:
    purpose: "Feed automated spam ML models, toxicity classifiers, and moderator report queues"

execution_paths:
  synchronous_path: "Atomic Redis Lua CAS state transition, publish vote-events to Kafka with acks=all, return 200 within 50ms"
  asynchronous_path: "Perform database batch writes, counter refreshes, search indexing, and feed recalculation off the critical path"
  failure_semantics: "Kafka producer timeouts are treated as ambiguous outcomes, where event_id-based consumer idempotency makes replay or duplicate publication safe. The service reconciles Redis serving state with the durable event stream rather than assuming a timeout means the event was never committed. On definite pre-publish errors, Redis state is rolled back immediately."
  dead_letter_queue: "vote-events-dlq routed after 3 retries, raising an alert if consumer lag exceeds 60 seconds"

API Design

RESTful API contracts and TypeScript domain signatures for post publication, comment tree retrieval, voting, and community feed queries:

Client API Signatures

TypeScript interfaces defining operational signatures and parameters across services:

TYPESCRIPT
// Submit a new post to a subreddit community
createPost(
  subreddit: string,
  request: {
    title: string;
    content?: string;
    postType: "text" | "link" | "image" | "video";
    url?: string;
    flair?: string;
    crosspostParentId?: string;
  }
): Promise<{ postId: string; createdAt: string }>;

// Retrieve a post along with ranked, nested comment trees
getPostWithComments(
  postId: string,
  sort: "best" | "top" | "new" | "controversial",
  depth?: number,
  limit?: number,
  cursor?: string
): Promise<{ post: PostDetail; comments: CommentNode[]; hasMoreComments: boolean }>;

// Cast or adjust an upvote or downvote on a post or comment (asynchronous ingestion)
castVote(
  entityType: "post" | "comment",
  entityId: string,
  direction: 1 | -1 | 0
): Promise<{
  accepted: boolean;
  voteState: 1 | -1 | 0;
  eventId: string;
  approximateScore?: number;
}>;

// Fetch ranked posts within a specific subreddit
getSubredditFeed(
  subreddit: string,
  sort: "hot" | "new" | "top" | "rising",
  limit?: number,
  cursor?: string
): Promise<{ posts: PostSummary[]; nextCursor: string | null }>;

// Add a root comment or reply to an existing comment tree
addComment(
  postId: string,
  content: string,
  parentCommentId?: string
): Promise<{ commentId: string; path: string; depth: number; createdAt: string }>;

// Search across posts, comments, and subreddits
searchContent(
  query: string,
  entityType?: "all" | "post" | "comment" | "subreddit",
  subreddit?: string,
  sort?: "relevance" | "new" | "top",
  limit?: number,
  cursor?: string
): Promise<{ results: SearchResultItem[]; nextCursor: string | null }>;

// Give an award or gilding to a post or comment
giveAward(
  entityType: "post" | "comment",
  entityId: string,
  awardType: "silver" | "gold" | "platinum" | "helpful",
  message?: string,
  anonymous?: boolean
): Promise<{ awardId: string; entityId: string; awardCount: number; coinBalance: number }>;

Create Post

Publishes a new discussion thread or media reference within a specific subreddit:

HTTP
POST /api/v1/subreddits/systemdesign/posts
Content-Type: application/json
Authorization: Bearer <user_token>

{
  "title": "System Design Tips",
  "content": "Here are my top tips for large scale discussions...",
  "type": "text",
  "flair": "Discussion"
}

Get Post with Comments

Retrieves a post and its associated comment tree with cursor pagination:

HTTP
GET /api/v1/posts/post-928374?sort=best&depth=5&limit=200
Authorization: Bearer <user_token>

HTTP/1.1 200 OK
Content-Type: application/json

{
  "post": {
    "post_id": "post-928374",
    "subreddit": "systemdesign",
    "title": "System Design Tips",
    "score": 420
  },
  "comments": [
    {
      "comment_id": "comm-8472",
      "content": "Great post!",
      "score": 150,
      "wilson_score": 0.942,
      "depth": 0,
      "replies": [
        {
          "comment_id": "comm-8473",
          "content": "Agreed!",
          "score": 42,
          "wilson_score": 0.881,
          "depth": 1,
          "replies": []
        }
      ]
    }
  ],
  "has_more_comments": true
}

Vote on Entity

Submits or clears a vote on a target post or comment:

HTTP
POST /api/v1/vote
Content-Type: application/json
Authorization: Bearer <user_token>

{
  "entity_type": "post",
  "entity_id": "post-928374",
  "direction": 1
}

HTTP/1.1 200 OK
Content-Type: application/json

{
  "accepted": true,
  "vote_state": 1,
  "event_id": "evt-7f9b2c3d-e45a",
  "approximate_score": 421
}

Get Subreddit Feed

Fetches a paginated, ranked feed of posts within a community:

HTTP
GET /api/v1/subreddits/systemdesign/posts?sort=hot&cursor=cur_h93f82&limit=25
Authorization: Bearer <user_token>

Add Comment

Appends a root comment or a reply to an existing comment tree:

HTTP
POST /api/v1/posts/post-928374/comments
Content-Type: application/json
Authorization: Bearer <user_token>

{
  "parent_comment_id": "comm-8472",
  "content": "This is a great point!"
}

Give Award

Bestows a community award or gilding on a post or comment:

HTTP
POST /api/v1/posts/post-928374/awards
Content-Type: application/json
Authorization: Bearer <user_token>

{
  "award_type": "gold",
  "message": "Outstanding technical depth!",
  "is_anonymous": false
}

HTTP/1.1 200 OK
Content-Type: application/json

{
  "award_id": "awd-901842",
  "entity_id": "post-928374",
  "award_type": "gold",
  "total_awards": 17,
  "coin_balance": 1850
}

Search Content

Executes multi-entity full-text queries across posts, comments, and subreddits:

HTTP
GET /api/v1/search?query=distributed+caching&type=all&sort=relevance&limit=25
Authorization: Bearer <user_token>

HTTP/1.1 200 OK
Content-Type: application/json

{
  "results": [
    {
      "entity_type": "post",
      "entity_id": "post-928374",
      "subreddit": "systemdesign",
      "title": "System Design Tips: Distributed Caching",
      "score": 420
    },
    {
      "entity_type": "comment",
      "entity_id": "comm-8472",
      "post_id": "post-928374",
      "body": "Redis cluster with consistent hashing resolves hot spot keys.",
      "score": 150
    }
  ],
  "next_cursor": "eyJvZmZzZXQiOjI1fQ=="
}

Common Error Responses

Standardized error payloads returned across search and forum endpoints:

400 Bad Request: invalid input, missing required fields, or malformed JSON payload
401 Unauthorized: missing or invalid authentication token or API key
403 Forbidden: authenticated caller lacks required permissions for this resource
404 Not Found: requested resource ID does not exist
409 Conflict: duplicate write or version conflict, retry with a unique idempotency key
422 Unprocessable Entity: syntactically valid request failed semantic business validation
429 Too Many Requests: rate limit quota exceeded, client should honor Retry-After header
500 Internal Error: unexpected server failure, retry safely with an idempotency key
503 Service Unavailable: downstream dependency is unavailable or overloaded, retry with exponential backoff
504 Gateway Timeout: search index shard responded slowly, narrow query parameters or retry

Data Model

Relational storage with materialized path indexes models comment hierarchies, while Redis sorted sets and hash structures power high-speed voting counters and feed ranking.

PostgreSQL: Posts (Sharded by subreddit_id)

Relational table storing post metadata, community ownership, and precomputed ranking scores:

SQL
CREATE TABLE posts (
    post_id             BIGINT PRIMARY KEY,
    subreddit_id        INT NOT NULL,
    user_id             BIGINT NOT NULL,
    title               VARCHAR(300) NOT NULL,
    content             TEXT,
    url                 TEXT,
    media_url           TEXT,
    post_type           VARCHAR(20) DEFAULT 'text', -- 'text', 'link', 'image', 'video'
    crosspost_parent_id BIGINT,                    -- references source post if crosspost
    score               INT DEFAULT 0,
    upvotes             INT DEFAULT 0,
    downvotes           INT DEFAULT 0,
    comment_count       INT DEFAULT 0,
    award_count         INT DEFAULT 0,
    hot_score           FLOAT DEFAULT 0,
    is_locked           BOOLEAN DEFAULT FALSE,
    is_removed          BOOLEAN DEFAULT FALSE,
    created_at          TIMESTAMP NOT NULL,
    INDEX idx_subreddit_hot (subreddit_id, hot_score DESC),
    INDEX idx_subreddit_new (subreddit_id, created_at DESC),
    INDEX idx_subreddit_top (subreddit_id, score DESC)
);

PostgreSQL: Comments (Materialized Path, Sharded by post_id)

Encodes hierarchical tree relationships in a dotted string path for single-query subtree retrieval:

SQL
CREATE TABLE comments (
    comment_id          BIGINT PRIMARY KEY,
    post_id             BIGINT NOT NULL,
    user_id             BIGINT NOT NULL,
    parent_id           BIGINT,                        -- null for top-level
    path                VARCHAR(1024) NOT NULL,        -- e.g. "000001.000003.000007.00000F" (6-char base-36 segments, 2.17B siblings/parent)
    depth               SMALLINT NOT NULL,
    child_seq           INT DEFAULT 0,                 -- monotonic counter for concurrent sibling allocation
    content             TEXT NOT NULL,
    score               INT DEFAULT 0,
    upvotes             INT DEFAULT 0,
    downvotes           INT DEFAULT 0,
    award_count         INT DEFAULT 0,
    wilson_score        FLOAT DEFAULT 0,
    controversial_score FLOAT DEFAULT 0,
    is_removed          BOOLEAN DEFAULT FALSE,
    created_at          TIMESTAMP NOT NULL,
    INDEX idx_post_path (post_id, path),
    INDEX idx_post_wilson (post_id, depth, wilson_score DESC),
    INDEX idx_post_controversial (post_id, depth, controversial_score DESC),
    INDEX idx_post_score (post_id, depth, score DESC)
);

PostgreSQL: User Votes (Authoritative Source of Truth)

Durable relational table enforcing the one-vote-per-user-per-entity invariant via a composite primary key:

SQL
CREATE TABLE user_votes (
    user_id         BIGINT NOT NULL,
    entity_type     VARCHAR(10) NOT NULL,      -- 'post' or 'comment'
    entity_id       BIGINT NOT NULL,
    direction       SMALLINT NOT NULL,         -- 1 = upvote, -1 = downvote
    updated_at      TIMESTAMP NOT NULL,
    PRIMARY KEY (user_id, entity_type, entity_id),
    INDEX idx_entity_votes (entity_type, entity_id)
);

PostgreSQL: Awards (Gilding and Community Appreciation)

Durable relational ledger storing awards and coins bestowed on posts and comments:

SQL
CREATE TABLE awards (
    award_id        BIGINT PRIMARY KEY,
    entity_type     VARCHAR(10) NOT NULL,      -- 'post' or 'comment'
    entity_id       BIGINT NOT NULL,
    giver_user_id   BIGINT NOT NULL,
    award_type      VARCHAR(32) NOT NULL,      -- 'silver', 'gold', 'platinum', 'helpful'
    coin_cost       INT NOT NULL DEFAULT 0,
    message         TEXT,
    is_anonymous    BOOLEAN DEFAULT FALSE,
    created_at      TIMESTAMP NOT NULL,
    INDEX idx_entity_awards (entity_type, entity_id, created_at),
    INDEX idx_giver_awards (giver_user_id, created_at)
);

PostgreSQL: Consumer Deduplication Ledger

Durable idempotency table preventing duplicate processing across Kafka consumer replays:

SQL
CREATE TABLE processed_events (
    consumer_group  VARCHAR(64) NOT NULL,
    event_id        UUID NOT NULL,
    processed_at    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (consumer_group, event_id)
);

Elasticsearch: Search Indices and Routing

Multi-index mappings and shard routing configurations across posts, comments, and subreddits:

YAML
indices:
  posts_index:
    routing: "subreddit_id (co-locates posts from same community on identical shard)"
    mappings:
      properties:
        entity_id:        { type: "keyword" }
        entity_type:      { type: "keyword" } # "post"
        subreddit_id:     { type: "keyword" }
        subreddit_name:   { type: "text", fields: { keyword: { type: "keyword" } } }
        author_id:        { type: "keyword" }
        author_username:  { type: "keyword" }
        title:            { type: "text", analyzer: "standard" }
        body:             { type: "text", analyzer: "standard" }
        post_type:        { type: "keyword" }
        score:            { type: "integer" }
        comment_count:    { type: "integer" }
        award_count:      { type: "integer" }
        created_at:       { type: "date" }

  comments_index:
    routing: "post_id (co-locates thread comments on identical shard)"
    mappings:
      properties:
        entity_id:        { type: "keyword" }
        entity_type:      { type: "keyword" } # "comment"
        post_id:          { type: "keyword" }
        subreddit_id:     { type: "keyword" }
        author_id:        { type: "keyword" }
        author_username:  { type: "keyword" }
        body:             { type: "text", analyzer: "standard" }
        path:             { type: "keyword" }
        score:            { type: "integer" }
        award_count:      { type: "integer" }
        created_at:       { type: "date" }

  subreddits_index:
    routing: "subreddit_id"
    mappings:
      properties:
        entity_id:        { type: "keyword" }
        entity_type:      { type: "keyword" } # "subreddit"
        name:             { type: "text", analyzer: "autocomplete", fields: { keyword: { type: "keyword" } } }
        title:            { type: "text" }
        description:      { type: "text" }
        subscriber_count: { type: "integer" }
        is_private:       { type: "boolean" }
        created_at:       { type: "date" }

Querying Comment Tree

Executes root comment selection and prefix-based subtree expansion:

SQL
-- Retrieve all top-level comments ordered by Wilson score ("best" sort)
SELECT * FROM comments
WHERE post_id = ? AND depth = 0
ORDER BY wilson_score DESC
LIMIT 25;

-- Retrieve top-level comments ordered by controversial score
SELECT * FROM comments
WHERE post_id = ? AND depth = 0
ORDER BY controversial_score DESC
LIMIT 25;

-- Retrieve nested replies under a specific comment branch using base-36 path prefix match
SELECT * FROM comments
WHERE post_id = ? AND path LIKE '000001.000003.%'
ORDER BY path
LIMIT 50;

Redis: Vote Counts, Awards & User Votes

In-memory hashes and strings managing real-time counter updates, awards tallies, fast deduplication, and idempotency:

YAML
# Score and Counter Cache (per post)
Key:     score:post:{post_id}
Type:    Hash
Fields:
  upvotes: 150
  downvotes: 3
  score: 147
  hot_score: 123456.789

# Score and Counter Cache (per comment)
Key:     score:comment:{comment_id}
Type:    Hash
Fields:
  upvotes: 42
  downvotes: 2
  score: 40
  wilson_score: 0.881

# Awards Cache (per post or comment)
Key:     awards:{entity_type}:{entity_id}
Type:    Hash
Fields:
  silver: 12
  gold: 4
  platinum: 1
  helpful: 8
  total: 25

# User Vote State (fast serving deduplication cache)
Key:     user_votes:{user_id}:{entity_type}:{entity_id}
Type:    String
Value:   "1" (upvote) | "-1" (downvote)
TTL:     30 days (2,592,000 seconds, with cache misses falling back to PostgreSQL user_votes)

# Counter Consumer Dedup Key (fast-path idempotency matching 7-day Kafka retention)
Key:     processed_events:counter:{event_id}
Type:    String
Value:   "1"
TTL:     7 days (604,800 seconds, falling back to the PostgreSQL processed_events ledger)

Redis: Ranked Feeds

Sorted sets maintaining precomputed post rankings per subreddit and personalized home feeds:

YAML
# Subreddit Hot Feed (Precomputed Sorted Set)
Key:     feed:sub:{subreddit_id}:hot
Type:    Sorted Set
Members: post_id
Scores:  hot_score (recomputed asynchronously by Ranking Worker)

# Subreddit Rising Feed (Short-Term Velocity Sorted Set)
Key:     feed:sub:{subreddit_id}:rising
Type:    Sorted Set
Members: post_id
Scores:  rising_score (recomputed every 60-120 seconds for posts < 12 hours old)

# Subreddit New Feed (Chronological Sorted Set)
Key:     feed:sub:{subreddit_id}:new
Type:    Sorted Set
Members: post_id
Scores:  created_at (written once on post creation with no worker recomputation needed)

# Personalized Home Feed per Active User
Key:     feed:home:{user_id}
Type:    Sorted Set
Members: post_id
Scores:  normalized_hot_score
TTL:     300 seconds (5 minutes, rebuilt proactively for active users and lazily on read)

Kafka Topics

Event topics decoupling content mutations from downstream cache and search consumers:

YAML
kafka_topics:
  post-events:
    description: "Post creation, text and media updates, and soft deletions"
  comment-events:
    description: "Comment creation, nested replies, edits, and deletions"
  vote-events:
    description: "Upvotes, downvotes, and vote cancellations carrying old and new state"
  award-events:
    description: "Community awards and gilding carrying entity references, award types, and coin transactions"

Fault Tolerance

Mitigation strategies for vote manipulation, thread retrieval timeouts, and ranking staleness under high concurrency:

General Resilience Matrix

ConcernSolution
Vote count accuracyReplicated Kafka provides durable, replayable event storage under configured acknowledgement and replication guarantees (replication factor 3, min.insync.replicas 2, acks=all), while Redis counters are periodically reconciled against durable PostgreSQL totals
Comment tree corruptionMaterialized path storage with atomic per-parent sequence allocation prevents path collision and broken ancestor links
Hot score stalenessBackground jobs recalculate community scores every 5 minutes, while streaming consumers apply immediate updates to viral posts
Database hot spots on popular postsRead replicas handle post text retrieval, while Redis absorbs high-throughput vote counter reads
Ranking computation worker failureSlightly stale rankings are served directly from Redis sorted sets to preserve platform availability during worker recovery
Redis cache failure / cold startRedis operates as an ephemeral serving layer, so if feed or counter caches are lost, subreddit feeds are rebuilt from PostgreSQL post indexes, vote counters from durable vote records, and home feeds lazily on the next user view

Specific: Handling Vote Manipulation

Defenses designed to neutralize bot rings and automated upvoting campaigns:

  • Unique vote constraints: Strictly enforces one vote per user per entity using unique composite database primary keys on (user_id, entity_type, entity_id) and Redis user vote keys.
  • Rate limiting: Restricts user accounts to a maximum of 100 vote actions per minute to mitigate automated bot scripts.
  • Vote fuzzing: Introduces minor random score fluctuations on public displays to prevent bot creators from verifying whether their automated votes succeeded.
  • Shadow banning: Silently ignores votes from flagged abusive accounts so that bad actors receive successful HTTP responses while their input is excluded from score aggregation.
  • IP clustering detection: Identifies coordinated voting rings by flagging abnormal spikes in identical voting patterns emerging from shared IP subnets.

Specific: Redis & Kafka Failure Recovery

Guarantees safe state recovery without data corruption or phantom mutations across serving and durable tiers:

  • Ambiguous timeout reconciliation: Definite pre-publish errors trigger immediate Redis rollback and an HTTP 503 error. For publish timeouts where broker commit status is ambiguous, the Vote Service avoids destructive rollback because downstream consumers use event_id deduplication to safely absorb retries or late deliveries, while background reconcilers ensure Redis serving keys match PostgreSQL durable state.
  • Event-level delta deduplication: While user-vote state upserts are naturally idempotent on (user_id, entity_type, entity_id), relative score deltas are not. Both vote-db-writer (via transactional processed_events in PostgreSQL) and vote-counter (via Redis SET processed_events:counter:{event_id} 1 EX 604800 NX, matching Kafka's 7-day retention window with PostgreSQL ledger fallback on cache miss) deduplicate by event_id prior to applying score deltas, eliminating duplicate counter increments during Kafka partition rebalances or consumer replays. Redis counters remain non-authoritative and are periodically re-synchronized against PostgreSQL durable scores.
  • Authoritative database backstop: When Redis user vote keys expire after 30 days or evict under memory pressure, subsequent vote requests consult PostgreSQL user_votes on cache miss to rehydrate the state before applying any new transition.
  • Counter reconciliation: Background reconciliation jobs periodically compare Redis hash counters against the count of active rows in PostgreSQL user_votes, fixing any transient drift caused by ungraceful Redis failovers.

Additional Considerations

Production patterns covering lazy comment loading, subreddit moderation tooling, cross-posting mechanics, hierarchical caching, and interview delivery strategies.

Lazy Comment Loading and Deep Thread Pagination

Loading massive comment trees requires bounded queries and incremental UI hydration:

  • Top-level truncation: For posts with over 10,000 comments, the client initially fetches only the top 200 comments ranked by Wilson score at depth 0.
  • Subtree expansion: Expanding a discussion branch triggers a query filtering on the path prefix such as WHERE post_id = ? AND path LIKE '000001.000003.%' LIMIT 50, avoiding recursive CTE overhead entirely.
  • Reply indicators: Each comment record stores a precalculated reply count, prompting clients to display an interactive expansion link.
  • Cursor pagination: Deep threads use lexicographical cursor pagination over the path column to ensure stable pagination even as new replies arrive.

Subreddit Moderation Architecture

Empowers community moderators while automating high-volume policy enforcement:

  • Moderator capabilities: Tools to remove offending content, ban repeat offenders, establish community-specific submission guidelines, and configure automated filters.
  • AutoModerator rules engine: A deterministic rule evaluator matching regular expressions, account age thresholds, and minimum user karma requirements.
  • Prioritized triage queue: Aggregates community member reports into an actionable review queue prioritized by report severity and frequency.

Cross-Posting Architecture

Supports sharing discussions across multiple communities while keeping engagement isolated:

  • Independent records: A cross-post creates a new row in the posts table referencing the source content through crosspost_parent_id.
  • Engagement isolation: Vote scores, comment trees, and moderation actions remain fully independent within each subreddit.

Multi-Tier Caching Architecture

Tiered caching layers minimize origin database load across dynamic voting and feed retrieval paths:

YAML
# Multi-Tier Caching Hierarchy
l1_client_cache:
  target: "Mobile applications and web browsers"
  ttl: "60 seconds"
  purpose: "Local cache of rendered posts and optimistic user vote state"

l2_edge_cdn:
  target: "CDN edge servers"
  ttl: "30 seconds"
  purpose: "Public subreddit front-page feeds for unauthenticated visitors"

l3_redis_cluster:
  target: "Distributed in-memory Redis cluster"
  ttl: "300 seconds"
  purpose: "Ranked feed sorted sets, hot post score hashes, and user vote records"

l4_database:
  target: "PostgreSQL primary and read replica pool"
  ttl: "Durable storage"
  purpose: "Authoritative relational source of truth"

Related Problems and Concepts

Threaded discussion and feed ranking patterns connect directly to feed generation in Design a News Feed System and social timeline fan-out in Design Twitter Timeline and Search. Direct messaging and chat infrastructure excluded from this scope is explored in Design a Real-Time Chat System, while high-volume vote counter scaling mirrors distributed counter designs in Like Count (High-Profile). Review foundational concepts in SQL vs NoSQL, Caching Patterns and Invalidation, Sharding and Partitioning, Redis Patterns for Interview Systems, Back-of-the-Envelope Estimation, and System Design Interview Patterns.

Interview Walkthrough

  • 25-Minute Interview Strategy

    Prioritize core data structures and asynchronous decoupling before exploring staff-level moderation and anti-abuse mechanics.

    • Requirements and Scope Definition (5 min)
    • Comment Tree Modeling: Materialized Path vs Adjacency List (6 min)
    • Asynchronous Vote Pipeline and Redis Counter Ingestion (5 min)
    • Ranking Algorithms and Redis Sorted Set Feed Generation (5 min)
    • Multi-Tier Caching, Edge CDN, and Fault Tolerance (4 min)
  • Clarify ranking models early by contrasting time-decayed logarithmic hot scores against Wilson confidence scoring for comments, establishing why each sort mode demands separate precomputation keys.
  • Model comment trees with materialized paths in PostgreSQL, demonstrating how dot-separated lineage paths enable single-query subtree fetches while keeping inserts efficient.
  • Structure the vote ingestion pipeline around Kafka and Redis atomic counters to avoid catastrophic row-level locking on popular threads in the relational database.
  • Leverage Redis sorted sets per community and sort mode to achieve sub-millisecond in-memory lookups, ensuring platform-level p99 feed delivery under 200 ms while background workers handle asynchronous recomputations.
  • Apply Back-of-the-Envelope Estimation to quantify write and read throughput: 50M daily active users submitting 5M posts, 50M comments, and 500M votes generate 555M write operations/day (~6,424/sec avg, 35K peak). At a 100:1 read-to-write ratio, total read traffic reaches 55.5B reads/day (~642K avg QPS), justifying asynchronous batching before hitting PostgreSQL and caching over 95% of reads in Redis and edge CDNs.
  • Establish multi-layer caching across client, CDN, Redis, and database tiers, tailoring time-to-live expirations to the staleness tolerance of each sorting mode.
  • Avoid the classic pitfall of calculating hot scores synchronously during feed read requests, which inevitably triggers p99 latency spikes on viral discussions.

Engineering Trade-offs

Key architectural decisions contrasting real-time vote ingestion pipelines, hierarchical tree storage engines, and algorithmic scoring mechanisms:

Vote Counting: Redis INCR vs Database Counters vs Log-Based Aggregation

Managing over 500M daily votes across viral content requires balancing write concurrency, persistence durability, and query latency:

Scale context: 500M votes/day (~6,000/sec average, 30,000/sec peak)
555M total writes/day, with 55.5B reads/day at a 100:1 read/write ratio

Option 1: Direct Synchronous Database Updates
  Query: UPDATE posts SET score = score + 1 WHERE post_id = ?
  Write bottleneck: Viral posts receiving thousands of concurrent votes lock rows
  Contention penalty: Severe database row-level locking degrades p99 latency
  Conclusion: Unviable for high-concurrency viral threads

Option 2: Atomic Redis Lua CAS with Asynchronous Kafka Pipeline (Recommended)
  Counter command: HINCRBY score:post:{post_id} score {delta}
  User state: Atomic Lua CAS on user_votes:{user_id}:post:{post_id} with 30-day sliding TTL
  Net delta rules:
    - 0 -> +1 (new upvote): delta = +1
    - 0 -> -1 (new downvote): delta = -1
    - +1 -> -1 (flip to downvote): delta = -2
    - -1 -> +1 (flip to upvote): delta = +2
    - +1 -> 0 (cancel upvote): delta = -1
    - -1 -> 0 (cancel downvote): delta = +1
  Advantages:
    - Atomic Lua script prevents race conditions between concurrent requests from the same user
    - O(1) in-memory operations handle tens of thousands of updates per second with zero row locks
    - Immediate 200/202 response returning accepted state, while PostgreSQL persistence runs asynchronously
  Failure semantics & durability:
    - Definite pre-publish failures trigger Redis rollback, whereas publish timeouts are treated as ambiguous and resolved via event_id consumer deduplication and stream reconciliation rather than blind rollback
    - Downstream consumers deduplicate event_id before applying score deltas: vote-db-writer writes to PostgreSQL processed_events table, and vote-counter checks Redis with a 7-day TTL (604,800s matching Kafka retention) backed by the database ledger, eliminating duplicate counter increments during Kafka partition rebalances or 7-day replays
    - Redis counter cache is explicitly non-authoritative, with periodic reconciliation jobs synchronizing Redis score hashes against PostgreSQL posts.score and comments.score durable truth
    - PostgreSQL user_votes unique constraint on (user_id, entity_type, entity_id) is the durable source of truth
    - Cache misses or expired keys fall back to PostgreSQL before accepting new state
  Trade-off: In-memory counters are volatile until Kafka consumers persist them

Option 3: Kafka Event Log with Streaming Aggregation (Apache Flink)
  Pipeline: Vote event -> Kafka topic -> Flink tumbling window -> Redis and DB update
  Advantages:
    - Strict durability with full replayability from the Kafka event log
    - Enables accurate historical score recalculation
  Trade-off: Introduces 5 to 30 seconds of processing latency and operational complexity

Production Decision: Atomic Redis Lua CAS paired with Kafka event publishing (acks=all) and asynchronous PostgreSQL batch writes. PostgreSQL user_votes is the authoritative backstop, and Redis counters are reconciled against durable state.

Comment Tree Storage: Materialized Path vs Adjacency List vs Nested Sets

Discussion threads frequently exceed 10,000 comments spanning more than 20 nesting levels:

Option 1: Adjacency List (parent_id references)
  Schema: comments(comment_id, post_id, parent_id, content, score)
  Full thread query: Recursive common table expression (WITH RECURSIVE)
  Trade-offs:
    - Simple relational schema and fast O(1) comment inserts
    - Recursive CTE queries become computationally prohibitive on deep 10,000-comment trees
    - Application-level iterative hydration causes severe N+1 query storms

Option 2: Materialized Path (Recommended)
  Schema: comments(comment_id, post_id, path, depth, child_seq, wilson_score, controversial_score, content, score)
  Example path: "000001.000003.000007.00000F" representing ancestor lineage from root (6-character base-36 segments, 2.17B siblings/parent)
  Subtree query: WHERE post_id = ? AND path LIKE '000001.000003.%'
  Advantages:
    - Single query retrieves entire subtrees without recursive joins
    - Lexicographical sorting on path yields natural pre-order display traversal for UI rendering
    - Encodes depth directly, simplifying UI indentation
    - Composite B-tree indexing on (post_id, path) isolates subtree queries to efficient single-index scans
    - Atomic per-parent sequence allocation (child_seq) prevents path collision during concurrent replies
    - Ranking consistency: Top-level comments use idx_post_wilson (depth=0, wilson_score DESC), while child branches expand by path prefix
  Trade-off: Moving comment branches requires updating descendant paths, which is exceptionally rare in discussion forums

Option 3: Nested Sets (Left and Right Boundary Values)
  Schema: comments(comment_id, post_id, lft, rgt, content)
  Trade-offs:
    - Fast read queries using boundary checks (WHERE lft > parent.lft AND rgt < parent.rgt)
    - Inserting a reply requires renumbering boundary values across the entire table
    - Catastrophic write amplification renders this model unusable for active discussions

Production Decision: Materialized Path architecture. Initial requests load the top 200 comments ranked by wilson_score, while user expansion triggers lazy subtree fetches using path prefix queries.

Ranking Algorithms: Wilson Score vs Hacker News vs Reddit Logarithmic Decay

Comparing content ranking algorithms across engagement velocity and fairness:

Option 1: Net Vote Difference (Upvotes - Downvotes)
  Calculation: net_score = upvotes - downvotes
  Weakness:
    - Heavily favors older content because accumulation increases over time
    - A post from 5 years ago with 100,000 upvotes permanently crowds out fresh discussions

Option 2: Hacker News Time-Decay Gravity Model
  Calculation: score = (points - 1) / (age_in_hours + 2)^1.8
  Weakness:
    - Simple and effective for post submission ranking
    - Lacks downvote weighting and cannot assess confidence across small sample sizes

Option 3: Wilson Score Confidence Interval (Recommended for Comments)
  Calculation: Lower bound of 95% confidence interval for a Bernoulli parameter
  Advantages:
    - Accurately balances positive vote ratio against total volume confidence
    - Prevents a comment with 1 upvote (100% positive) from outranking a comment with 100 upvotes and 2 downvotes (98% positive)
    - Naturally deprioritizes heavily controversial 50/50 vote distributions due to wider uncertainty intervals
    - Stored in the materialized wilson_score column and indexed for instant top-level retrieval

Option 4: Reddit Hot Ranking Model (Recommended for Posts)
  Calculation: log10(max(|ups - downs|, 1)) + sign(ups - downs) * (seconds_since_epoch / 45000)
  Key properties:
    - Logarithmic vote scaling: Each 10x increase in net upvotes yields 1 additional score unit, preventing hyper-viral posts from dominating forever
    - Temporal decay: Adds 1 score unit every 12.5 hours (45,000 seconds), ensuring fresh high-velocity posts naturally overtake older discussions

Option 5: Chronological New Sorting (Strict Timestamp Ordering)
  Calculation: score = created_at timestamp (Unix milliseconds or Snowflake ID timestamp)
  Key properties:
    - Evaluated once upon post or comment submission into feed:sub:{id}:new
    - Zero background worker recomputation required, strictly distinguishing New from engagement-based Hot/Top/Controversial rankings

Option 6: Rising Ranking (Short-Term Engagement Velocity)
  Calculation: score = (delta_ups_1h * 2 + delta_comments_1h * 3) / (age_in_hours + 0.5)^1.5
  Target window: Posts created within the last 6 to 12 hours
  Key properties:
    - Measures short-term engagement burst rather than cumulative vote mass
    - Enables breakout discussions to surface quickly before accumulating enough votes to reach the Hot feed
    - Maintained in Redis feed:sub:{id}:rising sorted sets by high-frequency Ranking Worker evaluation

💬Review

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...