SDSystem design
Start learning free

LESSON 83 OF 140 · PRACTICE PROBLEMS

SYSTEM DESIGN · INTERVIEW WALKTHROUGH

Design a Like Counter: System Design Interview Practice

The problem

"Design the like button" sounds like one counter, and the counting is the Distributed Counter again. But the interviewer wants the button end to end: the heart fills at once when Ana taps it, a second tap takes the like back, and under each post a line reads "Liked by Ana and 1,203 others", different for each viewer.

Say what carries over, then spend the time on what's new. The API: two calls on the heart and one read per post view, the user taken from the sign-in token.

POST   /posts/42/like    → 200 OK
DELETE /posts/42/like    → 200 OK

GET    /posts/42/likes   (Ben is looking)
  → 200 OK  {"count": 1204, "liked_by_me": false, "friends": ["Ana"]}

The numbers

From the interviewer, as for the Interview Framework's like button: 1 billion post views and 50 million likes a day; busy hours run about 5× the average.

quantity            estimate
likes, average      50 million ÷ 86,400 ≈ 600 a second
likes, busy hours   600 × 5 ≈ 3,000 a second
post views, busy    1 billion ÷ 86,400 × 5 ≈ 60,000 a second

In the Distributed Counter every view was a write. Here only a tap on the heart writes, 20 times fewer than the post views, so each like can save its row and its +1 at once, without batching. The busy path is the post view: about 60,000 a second, each showing the count, the viewer's own heart and friends' names.

Decision 1: the tap

The Like service usually answers in about 0.2 s, but on a train the call can take 2 s, or fail. A spinner until the server answers is never wrong but feels broken, so people tap again. Filling the heart and ignoring the answer is worse: a failed like stays on screen, then disappears on the next refresh.

The common answer is optimistic UI: fill the heart and show 1,204 at once, then send the call, because a like almost always succeeds. If the call still fails after a retry, empty the heart, put back 1,203 and say the like didn't save. The retry is safe, since a like is kept by its (user, post) row. And if Ana taps twice quickly, the app sends the calls for one post one at a time, so her like can't arrive after her unlike.

Decision 2: like and unlike

The heart sends the state Ana wants: POST to like, DELETE to unlike, never "toggle", which a retry would undo. The rule for an exact count: +1 only when a (user, post) row is really added, −1 only when one is really deleted, in the same transaction as the row.

call                         count
Ana's first like             +1
her like, retried            no change
Ana's unlike                 −1
her unlike, retried          no change
Ben's unlike (never liked)   no change
Ana's like again, later      +1

Without the rule, retries count Ana twice and stray unlikes drag the count down, even below 0 on a new post. Keep the like rows on the post's shard, beside its count, so one transaction covers both.

Decision 3: friends who liked

Ben's view of post 42 should read "Liked by Ana and 1,203 others". Say a person has about 300 friends. Looking each one up in the like rows costs 60,000 × 300 = 18 million lookups a second at busy hours, for one line of text. Reading all 1,204 like rows of the post is worse, and a viral post has millions.

Instead, keep each post's last 100 likers beside its count: they come in the same cache read, about 800 bytes per post at 8 bytes a user id. The Count service picks out the names in Ben's friend list, read once for his whole feed. A like puts its name at the front and an unlike takes it off. On a viral post the last 100 are mostly strangers and the line shows only the count: a fair trade for a nice touch.

Decision 4: liked by you

Ana's like is saved, and 0.3 s later she refreshes: the cache still holds post 42 as it was half a second earlier, with 1,203 likes and no Ana among the last likers. A heart taken from that entry would show empty right after her tap, and on a post with 1,204 likes her like from last week isn't in the last 100 either. Her phone can't be the source: her laptop or a new phone doesn't know what she tapped.

Her heart comes from her own (ana, 42) row, one lookup by key per post she sees. The row is saved before the Like service answers, so she always reads her own writes. That's about 60,000 lookups a second at busy hours: about 12 database servers at 5,000 each. The count may lag a second and is exact again once the cache refreshes; nobody can tell 1,203 from 1,204, but everyone notices a heart that empties.

Same pattern as the Distributed Counter

  • Counts kept ready: each post's count on its row, read from a cache kept a second.
  • One like per person: the (user, post) row, the key from the Interview Framework.
  • A hot post: a viral post with 50,000 views a second gets about 2,500 likes a second, half of the about 5,000 updates one row takes; beyond that, use sharded counters.

Summary

  1. Scope: the counter's pattern, plus the tap, unlike and friends' names.
  2. Data: a (user, post) row per like; each post keeps its count and its last 100 likers.
  3. API: POST to like, DELETE to unlike; the count moves only when a row does.
  4. Busy path: the post view, with the count, your heart and friends' names.
  5. Numbers: about 3,000 likes and 60,000 post views a second at busy hours.
  6. Scale: count and likers from a cache kept a second; your heart from your own row.
  7. The tap: optimistic UI, undone if the call fails.
Keep going with system designInteractive lessons from the building blocks to Twitter, Uber and payments, the way interviews ask. Free to start.
Start learning free