01What windows do charts need — arbitrary ranges, or fixed hour/day/month?
Fixed windows only: past hour, day, month, and all-time. Arbitrary time ranges are out of scope.
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 windows only: past hour, day, month, and all-time. Arbitrary time ranges are out of scope.
02How big can K get?
Up to 1,000 songs per chart.
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 fixed tiebreak — count first, then song id. The same query returns the same list every time, so charts never flicker.
05Region and genre charts - now or later?
Later. The global chart ships first; region and genre charts come in a later phase.
06A song gets taken down - when does it leave the chart?
Immediately — a taken-down song must vanish from every chart at once, never waiting 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.
02How fast must a chart read be?
Tens of milliseconds — opening a chart must feel instant.
03Do the numbers have to be exact?
Ask first, because it changes the design. The main path here is exact counts. These numbers may feed royalty reporting, so a systematic overcount is not acceptable.
04If something crashes mid-hour, is it acceptable to lose a few plays — or must every play eventually be counted?
Nothing may be lost. Every play must eventually be counted. A brief dip in freshness while recovering is acceptable.
05What about one song taking half of all plays?
A single hit song may grab a huge share of all plays at once. That must not slow counting or leave the charts stale.
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 can absorb this. The aggregator then consumes each partition in parallel.
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
Assume ~100M distinct songs × counter + key ≈ 50 B100M × 50 B ≈ 5 GB per window
Exact counting is affordable here. That 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 uses about 150× less state. The cost is a bounded overcount.
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 on every query would not be.
Decision example
Eight hundred thousand plays a second are coming in. The product question is tiny: the top 1,000 songs for four fixed windows, fresh within a minute.
Land every play in a partitioned event log. Inside the stream processor, count exact per-song plays in one-minute batches, then roll minutes into hours and hours into days — each window keeps its own counts. A small heap per window keeps the top 1,000 up to date continuously. 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. At 810K increments a second, that is a write storm, and the pre-aggregating stream absorbs it 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 windows multiply into per-region charts and dozens of windows, where a bounded overcount is acceptable.
If the product adds per-region and per-genre charts (hundreds of windows), I would split by popularity. Keep the hot songs on exact counters and put the long tail in a Count-Min Sketch — hot songs stay exact, and the tail costs almost nothing.
One complete picture first, then each path as its own diagram — the write path and the read path carry different traffic and justify different components.
Complete picture
Path 1
Pre-aggregation inside the stream turns 10,000 plays of a hit song into one counted write; the heap updates as counts change.
Path 2
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.
When is a Count-Min Sketch the right call here, and what exactly do you give up — in numbers?
Stick with exact counts. Here that's about 5 GB per window, 100M songs at ~50 bytes, and affordable. Reach for a Count-Min Sketch only when windows multiply into hundreds of region and genre charts. It cuts state 150x to 32 MB, but every count can run over, never under. Fine for a chart, wrong for royalties.
Reaching for the sketch by reflex — at 5 GB per window, exact counting is affordable and strictly more useful.
A hit single takes 30% of all plays. What happens to its partition, and how do you spread it without corrupting counts?
Salt the partition key. Add a small random suffix to the hit song's key so its plays spread across several partitions instead of overloading one. Then merge those partial counts into one song total before ranking. Skip the merge and the hit splits into several entries, each with a fraction of its real plays.
Salting the partition key but forgetting to merge the salted counts back into one song total.
It is 00:00:01 and everyone asks for the new hourly chart. Where does that answer come from?
From a snapshot that was already warmed. Before the boundary, the aggregator finalizes the closing window's top-1,000 and writes it to cache. So the first request after roll-over is a plain cache hit, just like any other. Compute the chart on that first request instead and you make the busiest second of the hour the slowest.
Computing the chart on the first request after roll-over — pre-warm the snapshot instead.
The stream processor crashes ten minutes into an hour window. What do the charts show, and how do counts recover?
Charts keep serving the last cached snapshot. They go stale for a minute or two, but never blank. A replacement processor loads its last checkpoint and replays the event log from that offset, recounting only the current window's tail. Nothing is lost, because every play lands in the log before it is ever counted.
Counting in processor memory without checkpoints — replay from the log is the whole safety story.
Prometheus-style time-series engines store counts over time — why do they fit badly here?
Two reasons, and cardinality is the first. 100M song IDs as tag values blows up the index and memory long before the write rate hurts. The query shape is wrong too. Those engines answer 'one series over time' well and 'top 1,000 across all series' badly — which is the question this system exists to answer.
Ignoring 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.