Social & Feed
Design YouTube Likes Counter
01
What the counter must do
What the counter must do
- A signed-in viewer can like or dislike a video, switch between the two, or clear their reaction.
- Each viewer counts at most once per video, regardless of retries, double taps or several devices.
- The watch page shows the like count, rounded for display ("48K", "1.2M").
- A viewer always sees their own reaction reflected immediately, even before the public count catches up.
- Creators see exact like and dislike totals in their dashboard; the public page shows likes only.
- Reactions from accounts later found to be fake are removed and the counts corrected.
- Accept 280,000 reactions per second at peak, with a single video receiving up to 40,000 per second.
- Serve 1.2 million count reads per second with p99 under 20 ms.
- Public counts converge within about 10 seconds of a reaction; creator totals within a minute.
- No lost reactions: once the button shows "liked", the like survives any single failure.
- Counts may be briefly stale but must never drift permanently from the true number of reactions.
Ask whether the public count must be exact or can be rounded, how stale it may be, and whether dislikes are shown. A rounded, slightly stale public number unlocks almost every simplification below. Also ask whether "who liked this" lists are needed. If they are, you need an index by video as well as by viewer.
02
Sizing reactions and reads
Sizing reactions and reads
Assume 6 billion reaction changes a day (likes, dislikes and undos), about 20 billion videos in the catalogue, and 30 billion watch-page loads a day that each read one count. Traffic peaks at about four times the daily average.
| Quantity | Working | Result |
|---|---|---|
| Reaction changes per day | assumption | 6B |
| Average write rate | 6B / 86,400 s | about 70,000 / s |
| Peak write rate | 4x average | about 280,000 / s |
| Hottest single video | premiere of a major music video | about 40,000 / s for the first hour |
| Count reads per day | 30B watch pages + feed and search cards | about 30B |
| Average and peak reads | 30B / 86,400 s, peak 3.5x | 350,000 / s and 1.2M / s |
| Stored like edges | 2 trillion active reactions x 40 B | about 80 TB, 240 TB with 3 replicas |
| Count rows | 20B videos x 32 B (likes, dislikes, watermark) | about 640 GB |
| Hot count cache | 200M videos with recent views x about 60 B | about 12 GB per cache cluster |
| Kafka throughput at peak | 280K events x about 120 B | about 34 MB / s |
280,000 writes a second across the whole platform is ordinary for a partitioned store. 40,000 writes a second to one counter row is not. Any design that increments a single row per like fails on exactly the videos that matter most, which is why the count is updated in batches instead.
A count is a few bytes and tolerates seconds of staleness, so it caches extremely well. The read path is a cache lookup with a short TTL, and the database behind it sees a tiny fraction of the traffic.
03
The reaction API
The reaction API
Because the client states the reaction it wants rather than an increment, a request retried five times after a flaky connection produces one change at most. The server works out the delta by comparing the new state with the old one.
04
Architecture overview
Architecture overview
flowchart LR
V["Viewer app"] --> API["Reaction API"]
API --> E[("Like edges, keyed by viewer")]
E -. "change stream" .-> K[["Kafka: reaction deltas, keyed by video"]]
K --> AG["Count aggregators"]
AG --> C[("Count store")]
AG -. "refresh" .-> RC[("Count cache")]
W["Watch page"] --> SA["Stats API"]
SA --> RC
RC -->|miss| C
SA --> E
F["Fraud pipeline"] --> API
R["Nightly recount job"] --> E
R --> CKeep two kinds of data and never confuse them. The like edge, one row per viewer and video saying like, dislike or none, is the source of truth. The count is a derived number that could be rebuilt from the edges at any time. Every component in the design follows from that split.
On the write path, the reaction API performs a single conditional write on the viewer's edge and learns the previous state in the same operation. If nothing changed, it returns immediately. If something did change, the change is captured from the edge table's change stream (or an outbox row written in the same transaction) and published to Kafka as a delta such as {video, likes: -1, dislikes: +1}, keyed by video ID.
Count aggregators consume those deltas, add them up in memory per video for about one second, and apply one combined update per video to the count store. A video receiving 40,000 likes a second therefore costs the count store one write a second, not forty thousand. After writing, the aggregator refreshes the cache entry so readers see the new value without waiting for a TTL.
On the read path, the stats API reads the count from a cache with a 5-second TTL and, for signed-in viewers, reads their own edge so the button shows the right state. A nightly job recounts edges for recently active videos and corrects any count that has drifted.
A wide-column or key-value store partitioned by viewer ID, with the video ID as the row key inside the partition. Writes from one hot video spread across every partition because they come from different viewers.
Partitioned by video ID so all deltas for a video meet in one aggregator. Retains three days of events, which is the replay window for any aggregator bug.
Stateless apart from one second of in-memory sums. Each owns a set of Kafka partitions and writes to the count store with a per-video offset watermark for safe replays.
Count rows keyed by video in a replicated store; a Redis or Memcached layer in front holds counts for videos being watched now.
05
Deep dive: exactly once, and no hot rows
Deep dive: exactly once, and no hot rows
How do you make sure each viewer counts exactly once? And how do you keep one viral video from turning into a single overloaded row? The first is solved at the edge table, the second in the stream.
flowchart TD
T["Viewer taps like"] --> CW["Conditional write on edge: none to like"]
CW --> CH{"State changed?"}
CH -->|no| OK1["Return current state"]
CH -->|yes| D["Emit delta: likes +1"]
D --> P["Kafka partition for this video"]
P --> B["Aggregator sums deltas for 1 s"]
B --> U["Apply +N to count row if offset is new"]
U --> CR["Refresh count cache"]Deduplication lives in the edge
Many designs put a Bloom filter or a Redis set in front of the counter to catch duplicates. That works, but it creates a second source of truth that can disagree with the first. Here the edge row itself is the dedupe record. The API reads and writes it in one atomic step, for example a lightweight transaction or a compare-and-set on a version, and the store tells it the previous value.
The delta then comes from the state transition, not from the request:
| Transition | Delta emitted | Typical cause |
|---|---|---|
| none to like | likes +1 | First like |
| like to none | likes -1 | Undo |
| like to dislike | likes -1, dislikes +1 | Changed mind |
| dislike to like | dislikes -1, likes +1 | Changed mind |
| like to like | no delta | Retry or double tap |
Because the edge is partitioned by viewer, a viral video's likes land on thousands of different partitions. The hottest key on this table is a single person tapping repeatedly, which a per-viewer rate limit handles. Two devices racing for the same viewer are serialised by the version check; the loser re-reads and recomputes its delta.
Batching turns a hot row into a cool one
Deltas for one video always go to one Kafka partition and therefore one aggregator. The aggregator keeps a map from video ID to a pending delta and flushes every second, or sooner if the map grows past 50,000 entries. A flush writes each pending delta with one increment and records the highest Kafka offset it included:
If an aggregator crashes after writing counts but before committing its Kafka offsets, its replacement replays the same events, builds the same batches and finds each one already applied. That gives effectively exactly-once counting without distributed transactions, because only one aggregator writes any given video and its offsets increase monotonically.
A single Kafka partition comfortably handles tens of thousands of small messages a second, so even the hottest premiere fits in one partition. If a video ever went beyond that, the next step would be to split its key into a few sub-keys, aggregate each separately and add them up on read, which is the classic sharded counter. You keep that in reserve; it is rarely needed once batching is in place.
Reading counts
The count on the watch page comes from cache, with a 5-second TTL and a refresh pushed by the aggregator on every flush. Expiry is staggered with a little random jitter so that a cache full of hot videos does not expire in one instant. Display rounding hides most small inaccuracies: once a video passes 10,000 likes, the page shows "48K", and a count that is ten seconds behind looks exactly the same.
A viewer's own reaction is never read from the count. It is read from their edge, which was written synchronously, so the button is correct immediately. The client also adjusts the displayed number locally by one after a tap, which hides the few seconds before the aggregate catches up.
Bugs, partial outages and fraud removals can all leave a count slightly wrong. A nightly job scans the edges of videos that had reactions in the last day, recounts them and repairs the count row. Since the edges are the truth, the count is always recoverable.
06
Decisions and their costs
Decisions and their costs
Exact deduplication without extra structures, a count that can be rebuilt at any time, and an audit trail of who reacted. The price is storing 2 trillion edges.
Correct by constructionSmaller storage and simple to start. But the set and the counter can disagree after a partial failure, and there is no way to recount or remove fake likes cleanly.
Drifts with no recoveryOne write per video per second regardless of popularity, and the cost of a viral hit stays flat. Counts lag by about a second plus pipeline delay.
Handles hot videosSpreads one video's increments over N rows and works without a stream. Every read has to sum N rows, and N must be chosen per video, which is awkward to manage.
Kept in reserveThe reaction API only touches one edge row, so its latency and availability do not depend on the count store at all. Readers see the new count a few seconds later.
Fast, isolated writesReaders see the change immediately, but every like now writes the hottest row in the system synchronously, and a slow count store slows the like button.
Couples the hot pathOver 99% of reads never reach the store. Staleness is bounded by the flush interval and the TTL, both a few seconds.
Cheap and fresh enoughAlways current, but 1.2 million reads a second against a store that only needs to handle a few thousand writes, for no visible benefit.
Wasted capacity07
Failure modes
Failure modes
A deploy goes wrong and the aggregator fleet restarts in a loop. Kafka keeps the deltas, but public counts freeze for twenty minutes during a big premiere.
Alert on consumer lag in seconds, not messages. Show cached counts as they are, since the viewer's own reaction still updates locally. Kafka retention of three days means nothing is lost while the fleet recovers.The cache entry for a trending video expires and thousands of concurrent readers miss together, all reading the same count row.
Refresh entries from the aggregator before they expire, add jitter to TTLs, and use request coalescing so one miss fetches while the others wait for its result.A viewer taps like on a phone and dislike on a laptop within the same second. Without care, both deltas apply and the counts end up off by one in each column.
The edge write is conditional on version. One write wins, the other re-reads the new state and computes its delta from there, so exactly one transition is counted per change.The aggregator has written counts for half its videos and has not committed offsets when its host disappears.
The replacement replays from the last committed offset. Videos already written are skipped by the offset watermark; the rest are applied once. No count is doubled.Fifty thousand fake accounts like one video in an hour. The fraud system flags them a day later.
The fraud pipeline sets those edges back to none through the normal API path, which emits negative deltas. Counts correct themselves, and creator dashboards show how many reactions were removed.A release emits deltas with the wrong sign for undos on one client version. Counts slowly inflate for a week before anyone notices.
The nightly recount repairs every recently active video from edges and reports how many counts it changed. A spike in repairs is itself an alert.08
Designs to avoid
Designs to avoid
The row for a viral video becomes the busiest key in the company. Lock contention or hot-partition throttling hits exactly when the most people are watching.
Every retry, double tap and replay adds another like, and there is no way to tell them apart afterwards.
You cannot answer "has this viewer liked it", cannot remove fake likes, and cannot recover after the count drifts.
It forces strong consistency and uncached reads for a number most viewers glance at once.
09
How to run the interview
How to run the interview
Split truth from aggregate early. Say that the per-viewer reaction is the truth and the count is derived. It answers dedupe, fraud removal and drift in one move.
Find the hot key. Point out that total writes are manageable but one viral video concentrates them on one counter. Interviewers wait for you to notice.
Offer batching before sharding. Coalescing deltas per video in the stream removes the hot row. Then describe sharded counters as the fallback and why you rarely need them.
Make the write idempotent. Desired state in the request, delta from the transition, conditional write on the edge. Walk through a double tap and a retry.
Explain exactly-once honestly. The offset watermark on the count row is what makes replays safe. Say what would break without it.
Agree on staleness. Get the interviewer to accept a few seconds of delay and rounded display. Then show how the viewer's own tap still appears instantly.
Plan for correction. Fraud removals and a nightly recount show that you expect counts to go wrong and know how to bring them back.
10
Growing the design in stages
Growing the design in stages
A likes table with a unique key on viewer and video, and a count computed on read. Perfectly correct and fine for a small site.
COUNT(*) gets slow on big videos, so a counter column is incremented with each insert. Hot videos start causing lock waits.
Counts move behind a cache and are updated from a queue, so the like button stops waiting on the counter.
Deltas are keyed by video and aggregated for a second before each write. Offset watermarks make replays safe, and a nightly recount repairs drift.
Each region records edges and aggregates locally; a global job merges regional totals for display. The single counter column is gone, and exact totals live only in creator tools.
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.