Reference path

The Comment System solution is read-only study material. Use it to compare your own design against a compact reference path before starting practice or after you finish a pass.

Review the problem detail before you compare trade-offs.

High-level design

The Comment API handles all CRUD operations and delegates upstream concerns to the Spam Filter (on creation) and Notification Service (on creation). The Comment Store is a relational database with an adjacency list schema. The Cache layer (Redis) holds serialized flat comment pages keyed by post ID and cursor token. A hot read path: the API looks up the cache by (post_id, cursor); on cache hit, it returns the serialized page directly; on miss, it queries the store for top-level comments plus the first reply level, serializes, caches with a 60-second TTL, and returns.

POST /posts/{postId}/comments
GET  /posts/{postId}/comments?cursor=X&sort=top
PATCH /comments/{commentId}
DELETE /comments/{commentId}
POST /comments/{commentId}/reactions
POST /comments/{commentId}/reports

Thread tree storage models

The adjacency list is the recommended choice for a system with a depth limit of 3 and high write volume. Queries reconstruct the tree with a 3-way join or a single recursive CTE. Materialized path enables faster subtree reads (WHERE path LIKE '1/42/%') but requires rewriting all descendant rows on subtree moves. Nested set provides O(log n) range-query subtree access but O(n) insertion cost -- a poor fit for comment workloads where writes are frequent.

Real-time updates

New comment events are published to a Redis Pub/Sub channel per post. Multiple push server instances each hold a subset of WebSocket connections and subscribe to the channel; the broker fans out the event to all push servers, which each deliver it to their connected viewers. This decouples the Comment API from individual connection state and scales fan-out horizontally by adding push server instances.

Spam filtering pipeline

The spam filter computes a composite score from three signals: per-user comment rate in the last 60 seconds (rate score), link density in content (link score), and content similarity to known spam patterns (pattern score). Score below 20: auto-allow. Score 20-70: store as pending, route to human review queue sorted by score descending so highest-confidence spam is reviewed first. Score above 70: auto-block with 422 response. Human reviewers approve or reject pending comments; approved comments transition to visible status and the cache is invalidated.

Trade-offs to study

Adjacency list versus materialized path: adjacency list has O(1) writes and requires recursive reads bounded by depth limit; materialized path has O(1) reads for subtrees but O(n) rewrite cost on moves. For a fixed-depth comment tree with no move operation, adjacency list is simpler and sufficient.

Push versus poll for real-time: WebSocket push provides sub-second notification but requires persistent connections that consume server resources. Long-poll fallback is compatible with all clients and self-heals on connection drop, but adds latency proportional to poll interval. For most comment systems, WebSocket with a poll fallback is the right combination.

Automated versus human moderation: automated scoring handles high-confidence spam at scale with no human cost. Human review is expensive but required for mid-confidence cases where false positives affect legitimate users. Calibrating the threshold range minimizes both missed spam and false human review queue volume.

Do not model learner practice diagrams as Mermaid in this reader. Authored Mermaid explains reference content; learner high-level-design work remains a future canvas submission flow.

Diagrams

flowchart LR
  Client[Client] --> CommentAPI[Comment API]
  CommentAPI --> SpamFilter[Spam Filter]
  CommentAPI --> CommentStore[(Comment Store)]
  CommentAPI --> Cache[(Cache / Redis)]
  CommentAPI --> NotificationService[Notification Service]
  Cache --> Client
  NotificationService --> Client
Client requests go to the Comment API. Comment creation passes through the Spam Filter before storage. Read requests are served from Cache when available; misses fall through to the Comment Store. The Notification Service receives creation events and alerts subscribers. Cache results return directly to the client.
flowchart TD
  subgraph AdjList["Adjacency List"]
    AL1["id=1, parent=NULL, content='Root'"] 
    AL2["id=2, parent=1, content='Reply A'"]
    AL3["id=3, parent=2, content='Reply A.1'"]
  end
  subgraph MatPath["Materialized Path"]
    MP1["id=1, path='1', content='Root'"]
    MP2["id=2, path='1/2', content='Reply A'"]
    MP3["id=3, path='1/2/3', content='Reply A.1'"]
  end
  subgraph NestedSet["Nested Set"]
    NS1["id=1, lft=1, rgt=6, content='Root'"]
    NS2["id=2, lft=2, rgt=5, content='Reply A'"]
    NS3["id=3, lft=3, rgt=4, content='Reply A.1'"]
  end
Three storage models for a 3-node comment tree: Adjacency List uses parent_id per row (simple writes, recursive reads). Materialized Path stores the full ancestor path string (fast subtree reads, expensive moves). Nested Set uses left/right range values (range-query subtree reads, expensive insertions).
sequenceDiagram
  participant Client
  participant API as Comment API
  participant Spam as Spam Filter
  participant Store as Comment Store
  participant Cache
  participant Notify as Notification Service
  Client->>API: POST /posts/{postId}/comments (content, parent_comment_id?)
  API->>Spam: score content and check rate limit
  Spam-->>API: score=12 (auto-allow)
  API->>Store: INSERT comment row
  Store-->>API: comment_id=42
  API->>Cache: DELETE post:{postId}:page:1
  Cache-->>API: invalidated
  API->>Notify: publish CommentCreated event
  Notify-->>Client: WebSocket push to parent author
  API-->>Client: 201 Created { id: 42, created_at: ... }
Comment creation goes through the Spam Filter for scoring, then to the Comment Store for persistence. On success, the API invalidates the affected cache page and publishes a CommentCreated event to the Notification Service which pushes to the parent comment author via WebSocket.
sequenceDiagram
  participant Author as Comment Author
  participant API as Comment API
  participant PubSub as Redis Pub/Sub
  participant Push1 as Push Server 1
  participant Push2 as Push Server 2
  participant Viewer1 as Viewer A (WS)
  participant Viewer2 as Viewer B (WS)
  Author->>API: POST /posts/{postId}/comments
  API->>PubSub: PUBLISH post:{postId}:comments (new comment payload)
  PubSub->>Push1: deliver event
  PubSub->>Push2: deliver event
  Push1->>Viewer1: WebSocket push (new comment)
  Push2->>Viewer2: WebSocket push (new comment)
On comment creation the API publishes an event to a Redis Pub/Sub channel for the post. Multiple push server instances subscribe to the channel and each fans out the event to their connected WebSocket viewers. This decouples the Comment API from individual WebSocket connections.
flowchart TD
  NewComment[New Comment Submitted] --> ScoreCompute["Compute Spam Score\n(rate score + link score + pattern score)"]
  ScoreCompute --> Threshold{Score threshold?}
  Threshold -->|score < 20| AutoAllow[Auto-Allow: store as visible]
  Threshold -->|20 <= score <= 70| HumanReview["Store as pending\nAdd to human review queue\n(sorted by score desc)"]
  Threshold -->|score > 70| AutoBlock[Auto-Block: store as rejected\nReturn 422 to author]
  HumanReview --> ReviewDecision{Reviewer decision}
  ReviewDecision -->|Approve| MakeVisible[Set status=visible\nInvalidate cache]
  ReviewDecision -->|Reject| KeepRejected[Set status=rejected\nOptionally warn author]
The spam pipeline scores each new comment using rate, link density, and pattern signals. Low scores auto-allow; mid-range scores route to a human review queue sorted by confidence (highest score first); high scores auto-block with a 422 response. Human reviewers approve or reject pending comments.