The 100 problems, solved / Social and feeds / 020 · Design a comments service

Design a comments service

Nested comments on billions of posts with moderation, pagination and hot threads.

Concepts this design leans on

01The design, in the six-step framework

1. Requirements

Functional

  • Comment on a post; reply to a comment (one level of nesting is required, deeper threads optional); edit and delete your own comment.
  • Read comments on a post sorted by newest, oldest or "top" (engagement), paginated; load replies under a comment on demand.
  • Likes on comments; moderation (hide, block words, report) by the post owner and by the platform.
  • Out of scope: real-time push of new comments, rich media in comments, search over comments.

Non-functional

  • 1 billion comments a month on 100 million posts; a viral post can receive 10,000 comments a minute and 100 million reads an hour.
  • Read a page of comments in under 100 ms at p99; write under 200 ms.
  • A user sees their own comment immediately after posting; others see it within seconds.
  • Availability 99.99 percent for reads; moderation decisions apply within a minute.
Back of the envelope

Writes: 109 / (30 × 86,400) ≈ 390 comments/s average, 5,000/s at peak globally; the hot post adds 170/s on one key.

Reads: assume 100 comment-page reads per comment written → 40,000/s average, 500,000/s at peak, concentrated on a few posts at any moment.

Storage: 109 × 12 months × 400 bytes ≈ 5 TB per year, plus likes; comfortable on a sharded store. The problem is hot posts and pagination, not volume.

2. Core entities

  • Comment: id (time-ordered), post id, parent comment id (null for top level), author, text, created-at, edited-at, status (visible, hidden, deleted), like count, reply count.
  • Comment like: (comment id, user id).
  • Post comment summary: post id → total count, top-N comment ids by score (cached).
  • Moderation rule: post owner or platform scope, blocked terms, hidden users.

3. API

  • POST /posts/{id}/comments body: text, optional parent id, client idempotency key → comment.
  • GET /posts/{id}/comments?sort=top|new|old&cursor=&limit=20 → comments with author snippets and the first two replies each.
  • GET /comments/{id}/replies?cursor= → replies in chronological order.
  • PATCH /comments/{id}, DELETE /comments/{id}, POST /comments/{id}/like, POST /comments/{id}/report.
  • POST /posts/{id}/moderation → owner hides a comment or a user.

4. High-level design

ClientsEdgeServicesDataread pagescreated / likedhiderefresh top pageApp / webLoad balancer+ rate limiterComment serviceModeration workersTop-commentsrecomputeComment DB(sharded by post id)Page cache(post → first pages)Comment eventsCounters

Posting and reading on a busy post:

  1. The comment service validates the text, runs a fast inline filter (blocked terms, link spam), writes the comment row on the post's shard, bumps counters, and returns. The author's client inserts the comment locally at once.
  2. An event goes to the queue: moderation workers run the heavier classifier and may hide the comment; the ranking worker recomputes the post's top-page list when enough new events accumulate.
  3. A read of the "new" sort is a range scan on (post id, comment id descending) with a cursor; the "top" sort reads the cached top list and hydrates. The first pages of hot posts live in the page cache with a few seconds' TTL.

5. Deep dives

Data model: how do nested replies and three sort orders coexist?

Store all comments of a post on the same shard, keyed by (post id, parent id, comment id), so top-level comments and the replies of one comment are each a contiguous range. Newest and oldest are the same range read in opposite directions; "top" is not a range at all, so it is a materialised list of the top few hundred comment ids by score, recomputed asynchronously and stored next to the post summary. Deep threads (replies to replies) are flattened in the UI by most products for a reason: a path-based key (post, root, path) supports arbitrary depth but makes "top replies" and counts harder, so state the one-level limit as a product decision and note the path-key alternative. A denormalised reply count and like count on each comment row avoid counting on read.

Pagination that survives inserts at 170 comments a second

Offset pagination breaks immediately: by the time a reader asks for page 2, 170 new comments have shifted everything. Use cursors: for time sorts the cursor is the last comment id seen (ids are time-ordered), and the next page is "ids less than cursor"; for the top sort the cursor is a position in the materialised list plus the list's version, so a reader pages through a consistent snapshot of the ranking even as it is recomputed behind them. New comments appear on the next refresh, not in the middle of a scroll. Each page is one range read plus one batched hydration of author profiles, which is why 100 ms is reachable.

The viral post: hot shard, hot cache, hot counter

All reads of a viral post hit one shard, which is deliberate for locality but dangerous for load: 100 million reads an hour is 28,000 a second on one key range. The page cache absorbs it: the first few pages in each sort order are cached per post with a 2 to 5 second TTL and refreshed by a single flight (one request rebuilds, the rest wait or serve stale) to avoid a stampede. Writes at 170 a second on one shard are fine for a database; the counters are not, so like and comment counts use sharded counters aggregated into the displayed value every few seconds. Read replicas of the hot shard take the uncached deep pages. If one post exceeds what a shard can absorb, the escape hatch is a per-post override that spreads its comments over several sub-shards by comment id and merges at read time; mention it as the ceiling, not the default.

Moderation and rate limiting without slowing honest users

Inline checks are cheap and synchronous: a blocked-term list per post owner and platform, a link count limit, and a per-user rate limit (comments per minute, replies per thread). The expensive classifier runs asynchronously and can hide a comment seconds later; readers who already fetched it will see it disappear on the next page refresh, which products accept. Hidden comments stay in the database with a status so the author still sees their own (shadow moderation) and appeals are possible. Owner actions (hide a comment, block a user from their posts) are rules checked at read time for that post, so they apply instantly without rewriting rows.

Edits, deletes and what readers see

An edit updates the row in place, stores the previous text in an edit-history table for moderation, and marks the comment as edited; cached pages that contain it expire within their short TTL, so a reader may see the old text for a few seconds, which products accept. A delete sets the status to deleted rather than removing the row, so replies under it keep their parent and the thread structure survives; the UI shows a placeholder. Counters are decremented asynchronously from the delete event. A user who deletes their account triggers a background job that walks their comment index (author → comment ids, maintained at write time) and deletes each one, which is the only time comments are touched by author rather than by post.

6. What a strong answer sounds like

  • Lay out the key design (post, parent, id) and show how two of the three sorts are range reads and the third is materialised.
  • Reject offset pagination with the 170-per-second argument and describe snapshot cursors for the top sort.
  • Treat the viral post as the central scaling problem: cache with single-flight refresh, sharded counters, replicas, and a sub-shard escape hatch.
  • Split moderation into inline cheap checks and asynchronous classification.
  • Expect follow-ups on notifying the post owner, on edit history, and on deleting a user's comments everywhere (an asynchronous job over the author's comment index).
  • Common mistake: sharding comments by comment id, which turns every page read into a scatter.

02Where this question was reported