Design Top-K Trending

Find the most viewed videos of the last hour, day and month: exact versus count-min sketch counts, sliding windows, merging partitions, precomputed reads.

Case study33 minLesson 2 of 7
On this page
  1. Clarify the problem
  2. Estimate the scale
  3. Define the API
  4. Model the data
  5. High-level design
  6. Deep dives
  7. Bottlenecks and failures
  8. What a strong answer sounds like
  9. Follow-up questions

A video site has a "Trending" tab: the 100 most viewed videos in the last hour, the last day and the last month. Behind the tab is a question that sounds like a single ORDER BY and is not: billions of views a day, hundreds of millions of distinct videos, windows that slide forward every minute, and a page that millions of people load.

Interviewers like it because it tests whether you do the arithmetic before reaching for a clever structure: sometimes exact counting fits, and sometimes you need a sketch and an honest statement of its error. The steps follow A Framework for Any Design. Try each "Your turn" box before you read on.

Clarify the problem

The first five minutes
Interviewer

Design a system that shows the top K most viewed videos.

You

Over which time ranges, and how big is K?

Interviewer

The last hour, the last day and the last month. K is at most 100.

You

Are those sliding windows, "the 60 minutes up to now", or calendar ones, "since 10:00"? And how fresh must each list be?

Interviewer

Sliding. The hour list should move every minute or so; the month list can be an hour old.

You

Does the order have to be exact, or is "very close" fine?

Interviewer

Close is fine as long as a clearly top video never goes missing.

You

Then reads will be one lookup of a precomputed list, and I'll pick exact or approximate counting per window by its memory cost. I'll take a clean stream of view events as given; defining and filtering views happens upstream.

  • Functional: return the top K (K ≤ 100) videos by views over the last hour, day and month, with their view counts.
  • Non-functional: the hour and day lists refresh every minute and the month list every hour; reads return in under 50 ms at any load; a video with clearly more views than the 100th is never missing; counts within about 5% are fine for the month.
  • Out of scope: defining a view, fraud filtering, personalised trending, arbitrary time ranges.

Estimate the scale

The scenario: 5 billion views a day. In a typical hour about 30 million distinct videos get a view, about 150 million in a day and 500 million in a month. A counter entry (video id, count and hash-map overhead) takes about 50 bytes.

  • Views: 5 × 109 ÷ 86,400 s ≈ 58,000 a second on average; at 3x peaks, about 175,000 a second.
  • Hour window, exact: to slide by the minute, keep one count map per minute (assume 2 million distinct videos a minute) plus running totals: 60 × 2 × 106 × 50 B + 30 × 106 × 50 B = 6 GB + 1.5 GB = 7.5 GB.
  • Day window, exact: one map per hour: 24 × 30 × 106 × 50 B + 150 × 106 × 50 B = 36 GB + 7.5 GB = 43.5 GB.
  • Reads: say the trending page peaks at 20,000 loads a second. Every one of them asks the same three questions.
Estimate: Exact counts for the month

How much memory do exact counts for a sliding 30-day window take?

You know
  • One count map per day, so the oldest day can be subtracted: 150 million distinct videos a day
  • Running totals for the month: 500 million distinct videos
  • About 50 bytes per entry

Show the working
  1. Daily maps: 30 × 150 × 106 × 50 B = 225 GB
  2. Running totals: 500 × 106 × 50 B = 25 GB
  3. Total: 250 GB

Estimate 250 GB (anything from 80 GB to 800 GB is a good answer)

7.5 GB for the hour spread over 32 tasks is trivial. 250 GB is not, which is why the month gets a different answer below.

Your turn: Define the API

Write the read endpoint. What parameters does it take, and what does it refuse?

Compare with a model answer

One GET with a window name and a limit up to 100. It refuses anything that is not precomputed, such as an arbitrary time range or K = 1,000, because every allowed request must be a single lookup.

Define the API

HTTP API
GET /v1/trending?window=hour&limit=50
  200 { "window": "hour", "as_of": "2026-11-14T10:06:00Z",
        "items": [ { "video_id": "V7", "title": "...", "views": 48210 }, ... ] }
  400 Bad Request   window is not hour, day or month, or limit > 100
  Cache-Control: max-age=30
 
View events (internal, from the video service to the log):
  { "video_id": "V7", "count": 3, "second": "10:05:12" }   -- pre-summed per server per second

The decision worth arguing is what the API refuses. "Top 50 for 14:00 to 15:30 last Tuesday" turns a lookup into a scan over billions of events. Three windows and K ≤ 100 keep every read to one key; ad hoc ranges belong in a warehouse, a different system.

View events are pre-summed: one event per video per server per second. A viral video watched 20,000 times a second on 500 servers becomes at most 500 events a second.

Your turn: Model the data

What state does the counting side keep for the hour window, and what does the read side store?

Compare with a model answer

The counting side keeps per-minute count maps and running totals in memory, partitioned by video id and checkpointed. The read side stores one small, finished list per window in a key-value store: about 100 entries, already sorted, with titles attached.

Model the data

State and results
Counter task state (one task per log partition, 32 partitions)
  minute_buckets : ring of 60 maps   video_id -> views in that minute
  hour_totals    : map               video_id -> sum of the 60 buckets
  hour_buckets   : ring of 24 maps   (same idea for the day)
  day_sketches   : 31 daily count-min sketches + 1 running month sketch
  candidates     : video ids whose month estimate passed a floor
  checkpoint     : state + log offsets, written to object storage every minute
 
Results (key-value store, about 10 KB per value)
  key   "trending:hour" | "trending:day" | "trending:month"
  value { as_of, version, items: [ {video_id, title, thumbnail, views} x 100 ] }

The counting state is a stream processor's keyed state, kept next to the computation and snapshotted for recovery; nothing on the read path touches it. The results are three keys read constantly and written once a minute: a key-value store or an in-memory cache with a durable copy. Titles are attached when the list is built, so a read never joins.

Your turn: High-level design

Sketch the path from a view to the trending page. Where are counts kept, where do the partitions' answers meet, and what does a page load touch?

Compare with a model answer

Views go into a log partitioned by video id. Counter tasks keep sliding-window counts and emit their local top 100 every minute. A merger combines the 32 lists into the global top 100 and writes it to a results store. The page reads one key, through a CDN.

High-level design

Counting is partitioned by video; one small merger builds each list; a page load is a single key read, mostly served by the CDN.Video servers to View log (views per sec); View log to Counter tasks; Counter tasks to Top-K merger (local top 100); Top-K merger to Video metadata (titles); Top-K merger to Results store (write list); Trending page to Trending API (via CDN); Trending API to Results store (GET key).Video serversView logCounter tasksTop-K mergerVideo metadataTrending pageTrending APIResults storeviews per seclocal top 100titleswrite listvia CDNGET key
Counting is partitioned by video; one small merger builds each list; a page load is a single key read, mostly served by the CDN.
One minute of the hour list, a crash, and a stale writeUse the arrows or ← → keys
  1. One minute of the hour list, a crash, and a stale write, step 1Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksTop-K mergerVideo metadataTrending pageTrending APIResults store
    V7 this minute
    0

    1At 10:05:12 one video server saw 3 views of V7 in the last second. It sends one event with count = 3 to the log partition for V7, partition 12.

  2. One minute of the hour list, a crash, and a stale write, step 2Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksV7 +3Top-K mergerVideo metadataTrending pageTrending APIResults store
    V7 this minute
    3
    V7 hour total
    48,820

    2Counter task 12 adds 3 to V7 in the 10:05 bucket and in the running hour total: two hash-map updates.

  3. One minute of the hour list, a crash, and a stale write, step 3Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksdrop 09:06Top-K mergerVideo metadataTrending pageTrending APIResults store
    V7 hour total
    48,210

    3At 10:06:00 the window slides. The 09:06 bucket falls out: the task subtracts each of its counts from the totals (V7 had 610 then) and reuses its slot for 10:06.

  4. One minute of the hour list, a crash, and a stale write, step 4Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksTop-K mergerlist 12 of 32Video metadataTrending pageTrending APIResults store
    V7 hour total
    48,210

    4The task scans its 1 million hour totals with a 100-entry min-heap and sends its local top 100 to the merger. V7 is its number 1.

  5. One minute of the hour list, a crash, and a stale write, step 5Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksTop-K merger3,200 to 100Video metadataTrending pageTrending APIResults store
    V7 global rank
    4

    5The merger has 32 lists, 3,200 entries. It keeps the best 100. The result is exact, because each video's whole count lives in one partition.

  6. One minute of the hour list, a crash, and a stale write, step 6Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksTop-K mergerVideo metadataTrending pageTrending APIResults storev10:06
    V7 global rank
    4

    6It fetches titles and thumbnails for the 100 videos and writes trending:hour with version 10:06, only if the stored version is older.

  7. One minute of the hour list, a crash, and a stale write, step 7Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksTop-K mergerVideo metadataTrending pageTrending APIResults store
    page reads
    1 key

    7A user opens the Trending tab. The CDN has a copy under 30 seconds old; on a miss the API reads one key.

  8. One minute of the hour list, a crash, and a stale write, step 8Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksdownTop-K merger31 + 1 staleVideo metadataTrending pageTrending APIResults store
    V7 global rank
    4

    8Counter task 12 crashes at 10:06:40. At 10:07 the merger gets only 31 lists. It reuses task 12's 10:06 list instead of dropping its videos, so V7 does not vanish from trending.

  9. One minute of the hour list, a crash, and a stale write, step 9Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksreplay 40 sTop-K mergerVideo metadataTrending pageTrending APIResults store
    V7 hour total
    48,610

    9A replacement task loads the 10:06 checkpoint (state and log offsets taken together) and replays partition 12 from that offset. About 40 seconds of events later, its counts are exact again.

  10. One minute of the hour list, a crash, and a stale write, step 10Video servers to View log; View log to Counter tasks; Counter tasks to Top-K merger; Top-K merger to Video metadata; Top-K merger to Results store; Trending page to Trending API; Trending API to Results store.Video serversView logCounter tasksTop-K mergerVideo metadataTrending pageTrending APIResults storekeeps v10:06
    stale writes applied
    0

    10Meanwhile a merger that paused during a long garbage collection wakes up and tries to write its old 10:05 list. The store holds version 10:06, so the conditional write is refused.

1 / 10

Deep dives

Windows: tumbling or sliding

A tumbling window is a fixed block, 10:00 to 11:00. At 10:01 its list is built from one minute of data, and a video that peaked at 9:55 vanishes at 10:00 sharp. A sliding window, the 60 minutes up to now, moves smoothly, but done literally it must forget each view exactly 60 minutes later.

The practical answer is a sliding window built from small tumbling buckets: 60 one-minute buckets for the hour. Each minute, add the newest bucket to the running totals and subtract the oldest; the work is proportional to one minute's distinct videos, not the hour's. For the day, use 24 hourly buckets plus the current partial hour, so the day list also refreshes every minute; its window is 23 to 24 hours long, which nobody notices on a trending page.

Merging partitions into one top K

Counting is split over 32 partitions. Each finds its local top 100; the merger picks the best 100 of the 3,200. Whether that is right depends entirely on how events were partitioned.

  • By video id (our choice): every view of V7 lands in partition 12, so partition 12 holds V7's true total. If V7 is in the global top 100, at most 99 videos anywhere beat it, so at most 99 in its own partition do, and it is in partition 12's local top 100. The merge is exact.
  • By server or at random: V7's views are spread over all 32 partitions. It could rank 101st in every one and still be first overall. Merging local top lists is then simply wrong, and you would need to ship every count to the merger or do a second exact pass on candidates.

The cost of partitioning by video is skew: a viral video loads one partition. Pre-summing per server per second cuts that to a few hundred events a second. The ad click aggregator shows the salted two-stage fix for when that is not enough.

Exact counts or a sketch

For the month, exact counts take 250 GB, mostly the long tail. A count-min sketch trades a small, one-sided error for a fixed amount of memory. It is a grid of counters, d rows by w columns, with one hash function per row. To add a video, hash it once per row and add to that cell in every row. To estimate its count, read those same d cells and take the smallest. Other videos that hash into the same cell can only add to it, so the estimate is never too low; taking the minimum picks the row where collisions hurt least.

A count-min sketch, 3 rows by 8 columnsUse the arrows or ← → keys
  1. sketch
    01234567
    h100000000
    h200000000
    h300000000
    total views
    0

    1An empty sketch: 3 rows, each with its own hash function, 8 counters per row.

  2. sketch
    01234567
    h103000000
    h200000300
    h300300000
    total views
    3

    2V7 gets 3 views. Its hashes are columns 1, 5 and 2, so add 3 to one cell in each row.

  3. sketch
    01234567
    h103002000
    h200000500
    h300300002
    total views
    5

    3V2 gets 2 views, hashing to columns 4, 5 and 7. In row h2 it lands on the same cell as V7: a collision, 3 + 2 = 5.

  4. sketch
    01234567
    h104002000
    h210000500
    h300300012
    total views
    6

    4V9 gets 1 view, hashing to columns 1, 0 and 6. It collides with V7 in row h1: 3 + 1 = 4.

  5. sketch
    01234567
    h104002000
    h210000500
    h300300012
    V7 estimate
    3
    V7 true
    3

    5Estimate V7: its cells hold 4, 5 and 3. Two are inflated by collisions; the minimum, 3, is exactly right.

  6. sketch
    01234567
    h104002000
    h210000500
    h300300012
    V4 estimate
    2
    V4 true
    0

    6Estimate V4, which was never viewed. It hashes to columns 1, 5 and 7, which hold 4, 5 and 2. Every one of its cells collided, so the sketch says 2 when the truth is 0.

  7. sketch
    01234567
    h104002000
    h210000500
    h300300012
    counters
    24
    videos
    any number

    7That is the whole trade: 24 counters for any number of videos, estimates that are never low and sometimes high. Wider rows mean fewer collisions; more rows mean less bad luck.

1 / 7

The guarantee is precise: with width w and depth d, each estimate is at most the true count plus (e ÷ w) × N, where N is the total of all counts added and e ≈ 2.718, with probability at least 1 − e−d. Each counter task keeps sketches for its own partition only, since every view of a video lands there. A month is 5 × 109 × 30 = 1.5 × 1011 views, so one task sees N ≈ 4.7 × 109. With w = 218 ≈ 262,000 and d = 5, an estimate is high by at most 2.718 ÷ 262,144 × 4.7 × 109 ≈ 48,600 views, with probability 1 − e−5 ≈ 99.3% for any one video. Assume the 100th video of a month has at least a million views: the error is under 5% of the counts that matter. Each sketch is 5 × 218 × 8 B ≈ 10.5 MB.

A sketch answers "how many for this video?" but cannot list the top ones, so pair it with a candidate set and a heap. Each task adds every view to today's sketch and to a running month sketch, then asks the month sketch for that video's estimate. If it passes a fixed floor, here 500,000 (half the assumed 100th place), the video joins the candidates. An estimate is never low, so any video whose true month count passes the floor joined on its last view. At midnight the task subtracts the day that fell out from the month sketch; cells add and subtract one by one when all sketches share hashes and width. Every hour it re-estimates the candidates, drops those under the floor, and sends its best 100, found with a min-heap, to the merger.

Counting for the month window
OptionGood atBad atPick it when
Exact counts in the streamExact, and the same code as the hour and day.250 GB of state to checkpoint and restore, almost all of it the long tail.Distinct items are few, or exact counts are a requirement.
Count-min sketches + candidate heap Usual default32 sketches of 10.5 MB per task, about 11 GB in all, 23 times less. Fixed size however many videos exist.Counts may be high by about 48,600, rarely more. The candidate floor must stay well under the 100th place."Close" is acceptable and the key space is huge. Our choice for the month.
Hourly batch over daily countsExact. Daily totals on disk in a warehouse; one group-by, sum, top 100.A second system and code path; scanning 30 × 150 million rows every hour.You already run a warehouse and an hour of lag is fine.

Precomputing so a read is one lookup

20,000 page loads a second ask one of three questions whose answers change once a minute; computing per request would redo the same work 1.2 million times a minute. The merger writes each finished list under one key, and the page reads it through a CDN with a 30-second lifetime. A slow pipeline means a slightly older list, never a slow page.

Each write carries a version (its minute) and succeeds only if newer than the stored one, or a merger that stalls and wakes late overwrites a fresh list with an old one, as in the tracer.

Bottlenecks and failures

At 10x, 1.75 million views a second and 10x the distinct videos, the hour state grows to 75 GB: go from 32 to 320 partitions, about 234 MB per task. Exact day counts reach 435 GB, the point where the day should move to sketches too. The merger handles 320 lists of 100 a minute easily, and reads do not change at all.

When this diesWhat happensWhat you do
A counter taskIts partition's counts stop.The merger reuses its last list; a new task restores the checkpoint and replays the log.
The mergerLists stop refreshing.Readers keep the last version. Run a standby; conditional writes stop a stale one from overwriting.
The results storeThe API cannot fetch lists.The CDN and the API's memory serve the last copy for minutes; replicate the store.
The view logVideo servers cannot send counts.They buffer per-second sums locally; lists freeze, then catch up. Playback is unaffected.
The metadata storeNew lists lack titles.Reuse titles from the previous list; fetch only new entries.

What a strong answer sounds like

LevelWhat the answer does
JuniorRuns GROUP BY video ORDER BY count LIMIT 100 over a views table; with prompting, caches the result.
MidCounts in a stream processor with windows, keeps a heap for the top K, and precomputes the results.
SeniorAsks sliding or calendar and exact or close, does the memory arithmetic per window, partitions by item so the merge is exact and says why, uses a sketch only where the numbers demand it and states its error bound.
Why a sketch, and whereStrong answer
Interviewer

Why not use a count-min sketch everywhere?

You

Because exact is affordable for the hour and the day: 7.5 GB and 43.5 GB spread over 32 tasks. The month needs 250 GB exact, almost all of it the long tail, so there each task keeps daily sketches of 10.5 MB, about 11 GB in all. With a width of 218 and depth 5 over a task's share of views, an estimate is high by at most about 48,600 views with 99.3% probability, under 5% of what a top-100 video gets in a month.

Interviewer

What does that error look like on the page?

You

Counts are shown rounded, like "1.2M views", so nobody sees it. Two videos with nearly equal counts near the 100th spot might swap. A clearly top video can't drop out because of the sketch, since its estimate is never low.

Follow-up questions

What changes for the most searched terms?

Normalise queries first. Distinct terms are far more numerous and most appear once, so exact maps are mostly waste: use a sketch plus heap for the day window too.

Rank by this hour's views divided by the video's usual hourly rate, with a minimum count so a jump from 2 to 20 does not win. Only the score changes.

How do you add per-country lists?

Make the bucket key (country, video). Memory grows with the pairs that actually occur, and the results store gets one key per country and window, still a single read each.

How do you stop people gaming the list?

Count only views that pass upstream checks, cap what one account or device adds per video per hour, and let moderation remove a video from candidates.

Why not a sorted set in an in-memory store?

For one window it works, as in Design a Leaderboard. Here every view would be a network write at 175,000 a second, and expiring old views from a sorted set still needs per-minute buckets. Counting inside the stream task avoids both.

Self-review: top-K trending0 of 13

After your attempt, tick what you really did. Be strict: "I thought about it" is not "I said it out loud".

Before drawing

Numbers and interfaces

Design and depth

Failure