InterviewKata
System design · Worked design D20 of 46 · Data and infrastructure · Long form

Design top-K and leaderboards

"Top 100 songs this hour", "trending hashtags", "your rank among 50 million players". All of these ask for the biggest few out of a huge, fast-changing set. With a few thousand items you sort a table; with hundreds of millions of events an hour you need sharded counting, time buckets and sometimes a sketch that trades a little accuracy for a thousand times less memory. It follows the interview framework phase by phase.

1. The prompt

Two related interview questions, often asked as one: "Design a real-time game leaderboard" (exact ranks for every player) and "Design top-K most played songs or trending items" (the top few out of billions of events, per time window).

They share the word "top" but not the solution: a leaderboard is a sorted index you can ask "where am I?", while top-K is a counting problem over a stream. Say which you're designing, or cover both in turn.

2. Frame the problem

Leaderboard: how many players?

50M in one game season.

Fits in memory in one sorted set.

Must ranks be exact?

Yes for the leaderboard (prizes); approximate is fine for trending.

Exact structures for one, sketches allowed for the other.

Top-K: how many events?

1B plays a day, peaks ~5× average.

~60,000 a second at peak: shard the counting.

Which windows?

Last minute, hour and day; globally and per region.

Time buckets, not one counter per item.

How fresh?

Leaderboard within a second; charts within a minute.

Charts can be precomputed and cached.

How big is K?

100.

The merge step stays tiny.

3. What it must do

  1. Leaderboard: update a player's score; show the top 100 and any player's exact rank and neighbours.
  2. Top-K: the top 100 items for the last minute, hour and day, globally and per region.

Not now:

4. How well it must do it

TargetWhy this number
Leaderboard updates visible within a second; ranks exact speed · consistencyPlayers watch their rank change as they play; prizes depend on it.
Top-K fresh within a minute; small errors acceptable for trending freshnessNobody notices song #73 and #74 swapped.
Scale: 50M players; 1B plays a day load shapeReads of the top lists are far more frequent than any single item's count.

5. Numbers that change the design

QuantityWorking→ so we…
Leaderboard size50M players × ~60–100 bytes in a sorted set~3–5 GB → so one Redis node, replicated for reads and failover
Score updatesSay 50,000 a second at peakEach O(log N) → so fine for one node; split by game or season beyond that
Play events1B a day, peak ~60,000 a secondToo many for one counter store → so shard counting by item id
Distinct itemsSay 100M songs, videos or hashtagsExact counters per window are large → so a Count-Min Sketch of a few MB where estimates are fine
Hour window60 one-minute buckets→ so slide by adding one bucket and dropping one

6. The nouns

scores     (game_id, player_id) → score, reached_at           (database: the source of truth)
lb:{game}  Redis sorted set: member player_id, score            (rebuilt from scores)
play       item_id, user_id, region, played_at                  (event stream, partitioned by item_id)
buckets    (shard, minute) → item_id → count
charts     (window, region) → [(item_id, count) × 100]           (cache)

7. The contract

POST /v1/games/{g}/scores {delta}           → ZINCRBY lb:{g} delta player   (player from the session)
GET  /v1/games/{g}/top?n=100                → ZREVRANGE lb:{g} 0 99 WITHSCORES
GET  /v1/games/{g}/rank/{player}            → ZREVRANK lb:{g} player  (+ ZREVRANGE around it for neighbours)
GET  /v1/charts/top?window=1h&region=IN&n=100  → precomputed list from cache

Data flow: leaderboard: (1) score update → (2) database + sorted set → (3) reads go to replicas. Top-K: (1) play events into a stream partitioned by item → (2) counter shards keep minute buckets → (3) each shard reports its local top K → (4) a merger combines them every few seconds → (5) charts cached for readers.

8. First working version

One database table of scores and one table of play events, queried directly.

8.1 "What's my rank?"

8.2 Top songs this hour

SELECT item_id, COUNT(*) FROM plays WHERE played_at > now() - 1h GROUP BY item_id ORDER BY 2 DESC LIMIT 100, on every request.

Parked for later (section 9 fixes each)

  1. A rank means counting everyone above you: millions of rows per request.
  2. The top-songs query scans an hour of events (~40M rows) every time.
  3. 60,000 events a second into one table.
  4. Counters for 100M items in every window.
  5. One viral item hammering one place.

9. Make it hold up

9.1 Exact leaderboards with sorted sets speed · exact

ApproachWhy
✗ BadSELECT … ORDER BY score DESC and COUNT(*) WHERE score > mine on every requestRank needs counting everyone above you: a scan over millions of rows per request.
~ GoodAn index on score in the database, with the top 100 cachedTop lists are fast, but "my rank" is still a large range count, and every update rewrites the index.
✓ GreatA Redis sorted set: a skip list plus a hash, so ZADD/ZINCRBY, ZREVRANK and range reads are O(log N)Rank and neighbours for any of 50M players in microseconds. Persist scores in a database too; the sorted set can be rebuilt from it.

9.2 Beyond one node scale

Most "leaderboards" are naturally many small ones: split by game, region or season, and each is its own sorted set. For one giant global board, shard by score range and keep a count per shard, so a rank is "players in higher shards + rank inside my shard". Ties are broken consistently (earliest to reach the score, folded into the stored score) so ranks don't flicker.

9.3 Top-K over a stream fresh · scale

9.4 Sliding windows with buckets fresh

Keep counts in one-minute buckets. "Last hour" = sum of the last 60 buckets; slide by adding the newest minute and subtracting the one that fell out. Hour and day buckets roll up from minutes, so "last day" sums 24 hour buckets instead of 1,440 minute ones. Old buckets are dropped; the raw stream is kept elsewhere if anyone needs to recount.

9.5 When memory is the limit: Count-Min Sketch scale · cost

A Count-Min Sketch is a small grid of counters: d rows, each with its own hash function, w columns wide. To count an item, add 1 to one cell per row; to estimate it, take the minimum of its cells. Collisions only ever add, so the estimate is never too low, and a wider grid means fewer collisions. Keep a small heap of the K items with the highest estimates, and you have approximate top-K in kilobytes instead of a counter for every item. Redis Open Source includes Count-Min Sketch and Top-K data structures.

Use it where small errors are fine (trending, analytics). For anything paid out (game prizes, royalties), use exact counts, or use the sketch only to pick candidates and count those exactly. Race the two in section 13.

9.6 Hot items scale

One viral item on one shard can overload it. Pre-aggregate on the producer side (count locally for a second, send one increment of +350 instead of 350 increments), or split the key as in Design D17 and add the parts during the merge.

9.7 The whole design after the deep dives

A score update and a chart refresh through the final design. Press Play or drag the timeline.

10. Interviewer follow-ups

Show "friends only" leaderboards.

Fetch the friends' scores (ZMSCORE) and sort the few hundred in the app; no separate sorted set per user.

Weekly reset.

A new sorted set per week (lb:{game}:2026-w42); the old one is archived to the database and expires. No mass delete at midnight.

Redis node fails.

Promote a replica; for anything lost since the last replication, rebuild from the scores table (the source of truth).

Top-K per region and per genre.

Each dimension is another key in the bucket; keep the combinations people actually use, and compute rare ones on demand from the stream archive.

Heavy hitters with error bounds.

Algorithms such as Space-Saving or Misra-Gries track the top items in O(K) memory with guaranteed error bounds; Count-Min with a heap is the common practical choice.

Cheating.

Validate score deltas server-side against game events, rate-limit updates, and hold prize-tier ranks for review before payout.

11. When things break

FailureWhat users seeHow we detect itWhat limits the damage
Sorted set primary downBrief stale ranksReplication healthReplica promotion; rebuild from DB
Counter shard downCharts miss some items brieflyConsumer lagRestart from stream offset
Merger downCharts stop updatingChart ageServe the last cached chart
Viral itemLag on one shardPer-key rateProducer pre-aggregation; key splitting

12. Wrap-up and the clock

Trade-offs we made:

What we'd watch:

At 10×:

A 45-minute interview

0–4Frame the problem (which "top", exactness, windows)
4–9Must do, how well, numbers
9–12Nouns, contract, data flow
12–18First working version, parked list
18–40Deep dives: sorted sets (9.1), top-K over a stream (9.3), windows (9.4), sketches (9.5)
40–45Follow-ups and wrap-up

A 60-minute interview

0–5Frame the problem
5–11Must do, how well, numbers
11–15Nouns, contract, data flow
15–23First working version
23–50Deep dives 9.1–9.6 and the final picture
50–60Follow-ups: friends, resets, heavy hitters; wrap-up
LevelWhat a strong answer shows
Mid-levelA sorted set for the leaderboard; counting plays and sorting for top-K; caching the result.
SeniorWhy ranks are O(log N) in a sorted set, sharding counts by item and merging local top-K exactly, minute buckets for sliding windows, cached results.
Staff+Count-Min Sketch and heavy-hitter algorithms with their error bounds, exact-vs-approximate per use case, giant leaderboards by score range, hot keys, regional charts and anti-abuse.

Common mistakes:

13. Try it, check yourself, read more

60,000 plays of 5,000 songs, where a few songs are hugely popular (a Zipf distribution, like real charts). Watch the top 10 race with exact counts on the left and a Count-Min Sketch with 4 rows on the right. Red bars are songs the sketch wrongly ranks in the top 10.

Exact counts

Count-Min Sketch + heap

Why does a Count-Min Sketch never underestimate?

Every cell an item maps to holds at least its true count; the minimum is the least inflated.

Why is merging each shard's local top K exact when counts are sharded by item id?

Any item in the global top K must be in its own shard's top K.

How do you get "top songs in the last hour" without rescanning events?

Add the newest bucket, drop the oldest: a sliding window for the price of 60 small arrays.

How expensive is "what's my rank?" among 50M players?

A skip list keeps span counts, so ZREVRANK walks down levels instead of scanning.
ResourceWhy
Sorted sets (Redis docs)Leaderboards with ZADD, ZRANK and ranges.
Count-min sketch (Redis docs)The sketch, its parameters and commands.
Design top-K (Hello Interview)Another walkthrough, for top-K videos.