Grokking the System Design Interview, Volume II
Vote

0% completed

​

Step 7: Detailed Component Design

  1. Synchronous updates: write the like and the count together
  1. Asynchronous counting: record the like now, count it later
  1. Hybrid: Kafka for the stored total, a cache for reads

Which approach we choose, and why

YouTube Likes Counter: Detailed Component Design in System Design Interview II: Step 7: Detailed Component Design (Video)

Alice (user 123) likes video XYZ1 in the minute it goes viral, along with thousands of other people. Her click stores her reaction in UserLikes, the table of who reacted to what, and raises the total of XYZ1 in ContentStats, the table of totals. The second write is the hard one, because thousands of likes a second all change the same row.

This lesson compares three approaches to that second write, each fixing the weakness of the one before:

  1. Write the like and the count together. Exact, but every like waits for one row.
  2. Record the like now and count it later. Batching removes the hot row, but reads still hit the database.
  3. Add a cache for reads. Reads move to memory. This is the approach we choose.

1. Synchronous updates: write the like and the count together

One database transaction writes Alice's row and adds one to the total of XYZ1, so the total is exact the moment the request returns, and there is nothing else to run.

The trouble starts when XYZ1 goes viral. Every like updates the same counter row, so transactions wait for its lock or fail and retry, and throughput falls exactly when traffic rises.

This suits a forum with a few hundred reactions a second, but at the 11,500 writes a second from Step 3, the single row is the limit, as the diagram below shows.

Approach 1. The reaction row and the count are written in one transaction.
Approach 1. The reaction row and the count are written in one transaction.

2. Asynchronous counting: record the like now, count it later

The second approach stores Alice's like now and counts it later. The service writes her row and publishes an event, "user 123 liked XYZ1", to Kafka, a durable log that consumers read in order while recording how far they have read (their offset). Workers read these events and update ContentStats in batches.

Batching removes the hot row. A worker collects up to 100 events or one second of them, groups them by item, and writes once per item. For XYZ1, a hundred likes become one update.

def flush(batch): changes = defaultdict(lambda: {"likes": 0, "dislikes": 0}) for event in batch: changes[event.content_id]["likes"] += event.likes_delta changes[event.content_id]["dislikes"] += event.dislikes_delta for content_id, change in changes.items(): # one write per item, not per event row = content_stats.get(content_id) content_stats.put(content_id, likes=row.likes + change["likes"], dislikes=row.dislikes + change["dislikes"]) consumer.commit_offsets() # only after the totals are written

The topic is partitioned by content_id, so one worker owns XYZ1 and needs no lock. It commits its offset only after writing, so a crash replays events instead of losing them, and Step 8 keeps a replay from counting twice.

Alice waits only for one insert and one append, and a burst of 50,000 likes waits in Kafka instead of overloading the database.

The cost is a total that lags by a second or two, and every count read, 250,000 a second by the Step 3 estimate, still goes to the database, as the diagram below shows.

Approach 2. The reaction is written, an event goes to Kafka, and workers update the durable count in batches.
Approach 2. The reaction is written, an event goes to Kafka, and workers update the durable count in batches.

3. Hybrid: Kafka for the stored total, a cache for reads

The third approach keeps Kafka and puts a Redis cache in front of the totals. Alice's click now does three things:

  1. Write her reaction to UserLikes.
  2. Raise the cached total of XYZ1 in Redis with an atomic command such as INCRBY, so two likes arriving together both count and Alice sees her like at once.
  3. Publish the event to Kafka, where workers keep ContentStats current in batches, as in approach 2.

When another viewer opens XYZ1, Redis answers in well under a millisecond. On a miss, the service reads ContentStats and copies the value into Redis (cache-aside). A viral video almost never misses, so the database sees mostly batched writes.

Each cache entry has a TTL (time to live), after which Redis deletes it. If the entry for XYZ1 expires while the workers are behind, the next read loads an older total, and viewers see the count drop and then rise. The TTL should therefore match the worst consumer lag the system accepts, not a fixed few seconds, as Step 10 explains.

The cost is that the cache, Kafka, and ContentStats each hold a version of the count and must agree, as the diagram below shows.

Approach 3. The cache is updated in the same request, and Kafka carries the change to the durable count.
Approach 3. The cache is updated in the same request, and Kafka carries the change to the durable count.

Which approach we choose, and why

We choose approach 3 because of two numbers from Step 3. With about 100 reads per write, only a cache serves 250,000 reads a second within 20 ms. With write peaks of 11,500 a second, a synchronous update waits on one hot row, while Kafka absorbs the burst.

The price is a stored total a few seconds behind, which Step 2 accepted: no viewer cares whether XYZ1 has 1,500,200 or 1,500,201 likes. At a smaller scale, approach 1 is still correct.

1. Synchronous2. Kafka only3. Kafka and cache
Write pathone transaction per reactionappend to a logappend to a log
Read pathdatabasedatabasecache, database behind it
Count freshnessexactseconds behindcurrent in the cache
Breaks atone hot row250,000 reads per secondnothing at this scale
Operating costnonea queue to runa queue and a cache

💡 In the interview: Reject two approaches with one reason each: "Synchronous updates share one row per popular video. Kafka alone still sends every read to the database." Then commit to the third and name its cost, a count a few seconds behind. If the scale drops to a few hundred reactions a second, switch to approach 1.

Reading Progress

0%


Vote for new content

On This Page

  1. Synchronous updates: write the like and the count together
  1. Asynchronous counting: record the like now, count it later
  1. Hybrid: Kafka for the stored total, a cache for reads

Which approach we choose, and why