Free full guide

Top K Songs (Spotify)

Design Spotify's Top-K most-played songs feature — compute top charts over sliding time windows at listening-event scale.

00

Practice checkpoints

The interview rhythm stays compact, so the page can spend attention on the actual design decisions.

  1. 01
    Clarify scope
  2. 02
    Requirements + scale
  3. 03
    API + data model
  4. 04
    Draw architecture
  5. 05
    Deep dive
  6. 06
    Trade-off decision
01

Requirements that shape the design

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.

Functional requirements

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

Non-functional requirements

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.

Keep asking — the interview is a conversation

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.

  • How long must past charts stay available — can someone pull last March's daily chart, or only the current windows?
  • Are bot and fraud plays in scope, or filtered upstream before they reach us?
  • Which timezone defines "a day" for the daily chart — UTC, or the listener's local time?
  • How long is all-time — does it ever reset?
  • If a label disputes a chart position, do we need an audit trail from the chart back to raw plays?
02

Numbers that force architecture decisions

Treat every estimate as a pressure that justifies a component: cache, queue, partition, replica, worker pool, or fallback path.

01

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.

02

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.

03

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.

04

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.

05

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

The numbers

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.

My choice

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.

Avoid

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.

Change if

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.

03

Architecture path

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

Overview — every component

Client AppsPlay Event Log(Kafka)WindowedAggregatorWindow CountsChart API +CacheTop-K Heap (perwindow)maintain the chartBatch Reconcilernightly recount
  • Plays flow left to right; the per-window heap keeps the top-1,000 answer warm continuously.
  • Dashed = off the real-time path: a nightly batch recount over the event log corrects any drift.
  • Takedowns happen at serve time: the Chart API filters delisted songs through a denylist, so removal is immediate and never waits for counts to be recomputed.

Path 1

Write path — count, then rank

Player ClientEvent Log(partitioned)Aggregator(1-min batches)Window CountsTop-K Heap

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

Read path — serve the snapshot

ClientChart APICache (perwindow)ChartSnapshot

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.

04

API and data model

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

PlayEvent

song_id · user_id · played_at

Append-only stream record; the log (with retention) is the source of truth for replay.

WindowCount

song_id · window (hour/day/month/all) · window_start · plays

Exact per-window counts, aggregated at coarser grains as windows roll up (hours → days → months).

ChartSnapshot

window · window_start · top_k: [song_id, plays][]

The precomputed answer the API serves; refreshed each minute, cached with a TTL.

05

Deep dive directions

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.

Focus

Exact or approximate

Ask

When is a Count-Min Sketch the right call here, and what exactly do you give up — in numbers?

Answer

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.

Avoid

Reaching for the sketch by reflex — at 5 GB per window, exact counting is affordable and strictly more useful.

Focus

One song eats the stream

Ask

A hit single takes 30% of all plays. What happens to its partition, and how do you spread it without corrupting counts?

Answer

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.

Avoid

Salting the partition key but forgetting to merge the salted counts back into one song total.

Focus

The window boundary

Ask

It is 00:00:01 and everyone asks for the new hourly chart. Where does that answer come from?

Answer

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.

Avoid

Computing the chart on the first request after roll-over — pre-warm the snapshot instead.

Focus

The aggregator dies

Ask

The stream processor crashes ten minutes into an hour window. What do the charts show, and how do counts recover?

Answer

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.

Avoid

Counting in processor memory without checkpoints — replay from the log is the whole safety story.

Focus

Why not a time-series database

Ask

Prometheus-style time-series engines store counts over time — why do they fit badly here?

Answer

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.

Avoid

Ignoring cardinality: 100M song IDs as tag values is exactly what time-series engines choke on.

Ready to practice?

Talk through Top K Songs (Spotify) out loud and get AI scoring on the explanation.

Practice this with AI →