Design like and view counters
Count a billion likes a day, show a number that is fresh enough, never double-count.
- Counters and approximate counting — Exact counters that shard, and sketches that count billions in kilobytes.
- Stream processing — Windows, watermarks and late events: computing over data as it arrives.
- Idempotency and exactly-once — Making a repeated request harmless, which is how 'exactly once' is really built.
- Caching strategies — Read-through, write-through, write-back, TTLs and invalidation.
- Sharding and partitioning — Splitting data by key so each machine holds a slice it can handle.
01The design, in the six-step framework
1. Requirements
Functional
- Count likes on posts and views on videos; show the count next to the item; a user can like once and unlike.
- The displayed number is fresh within seconds for most items; the exact value is available for reporting (creator dashboards, billing for views).
- Out of scope: who liked (a list), fraud detection beyond basic dedup, ranking that uses the counts.
Non-functional
- 1 billion likes a day and 10 billion views a day; a viral video receives 100,000 views a second for a short time.
- Reads of counts: every feed and watch page shows them, 100 billion reads a day.
- Never double-count a like from one user; views may be approximate in real time and exact daily.
- Available during a datastore failure: showing a slightly stale count is acceptable, showing an error is not.
Views: 1010 / 86,400 ≈ 116,000/s average, 500,000/s at peak globally; likes: 11,600/s average, 50,000/s at peak.
Count reads: 1011 / 86,400 ≈ 1.2 million/s average, 5 million/s at peak, so counts must come from memory close to the reader.
Like records for dedup: 109/day × 16 bytes = 16 GB/day, 6 TB a year; fine in a sharded store keyed by (post id, user id).
A single row incremented 100,000 times a second is far beyond any database row's capacity (a few thousand per second); the hot item needs splitting.
2. Core entities
- Like: (item id, user id, time); the existence of the row is the truth.
- View event: item id, viewer or device id, time, watch duration.
- Counter: item id → displayed count, updated-at; internally a set of shards per hot item.
- Daily total: (item id, day) → exact count from batch.
3. API
PUT /items/{id}/like,DELETE /items/{id}/like→ idempotent; response includes the new approximate count.POST /items/{id}/view→ fire-and-forget from the player with an event id for dedup.GET /items/counts?ids=...→ batched counts for a page of items.GET /items/{id}/stats?day=→ exact daily figures for creators and billing.
4. High-level design
A like and a view on a viral video:
- A like is an insert-if-absent of (item, user) on the item's shard; if the row existed, the request is a no-op. The service emits a +1 event only when the insert happened. Unlike is the mirror.
- A view is an event with the player's event id straight onto the stream, partitioned by item id so all events for one item go to one partition.
- The aggregator sums events per item per second (dropping duplicate event ids inside a short window) and adds the delta to the counter cache. The count readers see is at most a few seconds old.
- The nightly batch recounts from the archived events and like records and overwrites the cached counts, correcting any drift.
5. Deep dives
Why not increment a row per like, and what replaces it?
A row increment is a read-modify-write under a lock; a database row sustains a few thousand a second, so a 100,000-a-second burst queues and times out, and retries make it worse. Replace the hot row with aggregation: events flow to a partition, one consumer sums them in memory for a second, and writes one delta. The write rate on the counter becomes one per item per second regardless of popularity. For the small number of items hot enough to exceed one partition's throughput, split the item's events across several partitions by a hash of the viewer and have each sub-aggregator write its own counter shard; the read sums the shards (or a merger does, every second). The displayed count is approximate by design, with a bound the product accepts.
Never double-count a like
Likes are a set, not a counter: the (item, user) row makes the operation idempotent, so a double tap, a retried request or a replayed event cannot add two. The +1 event is emitted only on a successful insert, and the nightly batch's truth is the row count, so even if a delta event is lost or duplicated in the stream, the count is corrected within a day. The unlike path deletes the row and emits −1. Views have no user identity to key on for anonymous viewers, so they use a per-playback event id for short-window dedup and are honest about being approximate; billing-grade view counts are produced by the batch with stricter rules (minimum watch time, bot filtering).
Serving 5 million count reads a second
Counts live in an in-memory cache replicated into every serving region; the aggregator writes deltas to a primary and the regions receive the updates through the same stream, so a region reads locally. Pages ask for counts in batches (20 items per feed page) in one multi-get. Items with no cache entry (old, cold) fall back to the stored daily total, which is good enough for an item with no recent activity. If the cache tier is unavailable in a region, readers show the last value they had or the daily total rather than an error, which is the availability requirement made concrete.
Exactness, drift and the batch
Real-time deltas drift: a consumer crashes after updating the cache but before committing its offset and replays a second of events, or a duplicate slips through. The fix is not to make the stream perfect but to make the truth recomputable: all events land in the warehouse, and a nightly job recounts every item that had activity and writes the exact total back, replacing the live value. Creators' dashboards and any billing read the batch numbers, which are versioned by day. This two-speed pattern, approximate now and exact later, is the same one used for ad click aggregation.
The "liked by you" state at feed speed
Every feed page must also show whether the viewer already liked each of its 20 items, which is a point read on the like table per item. Batch those reads into one multi-get per page on the viewer's behalf, and cache the viewer's recent likes (a small set of item ids) in the session cache so repeat loads need no database read. A like made moments ago must show immediately for its author even though the counter is updated asynchronously: the client applies the optimistic +1 locally and the like service returns the cached count plus one, which is good enough until the aggregator catches up a second later.
6. What a strong answer sounds like
- Show the row-increment ceiling with a number and replace it with per-second aggregation on a partitioned stream.
- Make likes idempotent through the (item, user) row and derive the +1 event from the insert outcome.
- Describe read serving from a regionally replicated counter cache with batched gets and a stale-on-failure policy.
- Separate approximate real-time counts from exact nightly totals and say who reads which.
- Expect follow-ups on a count that must be exact in real time (a sharded counter with a consistent sum is possible but costly), on bot views, and on showing "liked by you" (a point read on the like row, cached per user).
- Common mistake: a single counter row per item, or reading the count by counting like rows.
02Where this question was reported
- IGotAnOffer · 'system that counts the number of clicks on YouTube videos'
- IGotAnOffer · Same, EM high-level list
- interviewkickstart.com · 'counters system for online services'
- interviewkickstart.com · Listed
- Blind · HC-requested extra round: start with a basic solution, then scale it to Google size
- Blind · Like count / aggregate for posts
- Taro · EM loop, offer accepted: 'count the number of clicks on YouTube Shorts'; medium-hard, many follow-ups.
- Glassdoor · Per search snippet (page 403): Glassdoor EM report of the same Shorts click-counting question.
- interviewkickstart.com · 'A counters system for online services'.