01What windows do charts need — arbitrary ranges, or fixed hour/day/month?
Fixed tumbling windows: past hour, day, month, and all-time. Arbitrary ranges are out of scope — that single constraint is what makes the design tractable.
Free full guide
Design Spotify's Top-K most-played songs feature — compute top charts over sliding time windows at listening-event scale.
The interview rhythm stays compact, so the page can spend attention on the actual design decisions.
Do not only state requirements. Ask for them. Each card pairs the design constraint with a clarification question you can say out loud before drawing the architecture.
01What windows do charts need — arbitrary ranges, or fixed hour/day/month?
Fixed tumbling windows: past hour, day, month, and all-time. Arbitrary ranges are out of scope — that single constraint is what makes the design tractable.
02How big can K get?
Up to 1,000 — small enough that the answer is always a precomputed list, never a scan.
03How fast must a new play show up in the chart?
Within about a minute — near-real-time, not instant.
04Two songs tie at rank K - what breaks the tie?
A deterministic tiebreak (count, then song id) - the same query must return the same list twice; charts cannot flicker.
05Region and genre charts - now or later?
Later, and priced honestly: every added dimension multiplies per-window state, so the base design names that cost up front.
06A song gets taken down - when does it leave the chart?
Immediately via a serve-time denylist filter - removal never waits for counts to be recomputed.
Out of scopeArbitrary time ranges (from/to queries) · Per-user or per-region personalized charts · Play-fraud detection
01What event rate should ingestion plan for?
Hundreds of thousands of play events per second at peak — the write path is a stream, not database inserts.
02How fast must a chart read be?
Tens of milliseconds — charts are precomputed and cached; a read never touches raw counts.
03Do the numbers have to be exact?
Ask — it changes the design. Main path here: exact counts. If memory matters more than exactness, a Count-Min Sketch shrinks state by orders of magnitude and the chart tolerates its small overcount.
04What happens when the aggregator crashes?
Replay from the event log plus checkpoints — at most the current window is recomputed; nothing is lost.
05What about one song taking half of all plays?
Partition by song ID with hot-key salting for the head of the distribution — one hit song must not melt one partition.
Real interviews probe far more than a tidy list. These are the scope questions that separate candidates who interrogate the problem from those who recite it.
Treat every estimate as a pressure that justifies a component: cache, queue, partition, replica, worker pool, or fallback path.
Event ingest rate
Tens of billions of plays per day (a deliberate 70B/day stress ceiling — an order beyond today’s streaming volumes)70B ÷ 86,400 s ≈ 810K events/s
Only a partitioned log absorbs this; the aggregator consumes in parallel per partition.
Pre-aggregation win
Aggregator batches counts per song per minute before writinga song played 10,000×/min → 1 write instead of 10,000
Stream pre-aggregation cuts store writes by 3-4 orders of magnitude for hot songs.
Exact-count memory
~100M distinct songs × counter + key ≈ 50 B100M × 50 B ≈ 5 GB per window
Exact counting is affordable here — which is why it is the main path, not the sketch.
Sketch alternative
Count-Min Sketch, 4 hash rows × 2M buckets × 4 B≈ 32 MB per window vs 5 GB exact
When per-window memory matters (many windows, many regions), the sketch trades a bounded overcount for ~150× less state.
Chart refresh cost
Top-1,000 maintained with a min-heap over count updatesheap update O(log 1,000) ≈ 10 comparisons per counted song-minute
Maintaining the chart continuously is cheap; recomputing it from scratch per query would not be.
Decision example
Eight hundred thousand plays a second coming in, and the product question is tiny: the top 1,000 songs for four fixed windows, fresh within a minute.
I would land every play in a partitioned event log, aggregate exact per-song counts in one-minute batches inside the stream processor, and roll minutes into hours, hours into days — each window keeps its own counts. A small heap per window maintains the top 1,000 continuously, and the API serves a cached snapshot that refreshes every minute and pre-warms at window boundaries.
What I would NOT do: count plays with database increments — 810K increments a second is a write storm the pre-aggregating stream absorbs for free. I also would not reach for a Count-Min Sketch by default: exact counts cost about 5 GB per window here, which is affordable, and exact numbers keep the door open for royalty-grade uses. The sketch is the right tool when window count multiplies — per-region charts, dozens of windows — and a bounded overcount is acceptable.
If the product adds per-region and per-genre charts (hundreds of windows), I would flip the head of the distribution to exact counters and the long tail to a Count-Min Sketch — hot songs stay exact, the tail costs almost nothing.
Draw the write path and the read path as separate lanes — they carry different traffic and justify different components.
Plays flow left to right; the heap keeps the answer warm continuously. Dashed = off the real-time path: a nightly batch recount over the log corrects any drift.
Pre-aggregation inside the stream turns 10,000 plays of a hit song into one counted write; the heap updates as counts change.
Reads never see raw counts. Snapshots refresh each minute and pre-warm at window boundaries, so the top-of-the-hour request is as fast as any other.
Before optimizing, make the contract inspectable: endpoints, entities, ownership, retries, and state.
GET/charts/top?window={hour|day|month|all}&k=100
res200 [{ song_id, plays }] (≤1,000 entries)
Serves the latest ChartSnapshot from cache — cache key charts:{window}:{window_start}, warmed on window roll-over so the boundary minute is never slow.
STREAMplay-events topic (partitioned by song_id + hot-key salt)
resconsumed by the windowed aggregator
The only write path. Producers fire-and-forget; durability comes from the log, not the producer.
Core entities
PlayEventsong_id · user_id · played_at
Append-only stream record; the log (with retention) is the source of truth for replay.
WindowCountsong_id · window (hour/day/month/all) · window_start · plays
Exact per-window counts, aggregated at coarser grains as windows roll up (hours → days → months).
ChartSnapshotwindow · window_start · top_k: [song_id, plays][]
The precomputed answer the API serves; refreshed each minute, cached with a TTL.
Pick one lane for the final third of the interview. Each lane gives you the topic, the interviewer question it should answer, and the failure mode to avoid.
FocusExact or approximate
AskWhen is a Count-Min Sketch the right call here, and what exactly do you give up — in numbers?
AvoidReaching for the sketch by reflex — at 5 GB per window, exact counting is affordable and strictly more useful.
FocusOne song eats the stream
AskA hit single takes 30% of all plays. What happens to its partition, and how do you spread it without corrupting counts?
AvoidSalting the partition key but forgetting to merge the salted counts back into one song total.
FocusThe window boundary
AskIt is 00:00:01 and everyone asks for the new hourly chart. Where does that answer come from?
AvoidComputing the chart on the first request after roll-over — pre-warm the snapshot instead.
FocusThe aggregator dies
AskThe stream processor crashes ten minutes into an hour window. What do the charts show, and how do counts recover?
AvoidCounting in processor memory without checkpoints — replay from the log is the whole safety story.
FocusWhy not a time-series database
AskPrometheus-style time-series engines store counts over time — why do they fit badly here?
AvoidIgnoring cardinality: 100M song IDs as tag values is exactly what time-series engines choke on.
Talk through Top K Songs (Spotify) out loud and get AI scoring on the explanation.