Case Study 3.4: How Spotify Wrapped serves a launch-day spike
Concept. Spotify Wrapped computes each user's yearly summary before launch and stores the result by user, so the launch-day spike serves precomputed records.
Intuition. In this example architecture, 700 million users request Wrapped on December 1. A request cannot scan 365 billion play events, so a batch job aggregates each user's listens before launch and stores one record keyed by user_id.
Figure: Precompute summaries, then look them upChronological storage scatters one user's listens across the disk and requires one thousand random reads in this example. Clustering the Listens table by user_id and listen_time groups that user's plays on neighboring pages. Before launch, the batch job reads them sequentially and writes one Wrapped summary row per user. On December 1, this example fetches the card with one hash-index lookup on user_id.
Figure 1. The design clusters Listens by (user_id, listen_time), so a batch job reads each user's year sequentially and writes one summary row per user. On December 1, the card fetch uses one hash-index lookup on user_id.
Two phases: batch and online
The batch phase aggregates a year of play events into per-user summaries before launch. The online phase serves those summaries to 700 million clients. Each phase needs an index that matches its access pattern.
Phase 1: offline aggregation
Before December, the batch job reads the Listens table to tally each user's top tracks. The worked example follows a user with 1,000 listens.
Option A: unclustered storage
Chronological storage scatters the example user's 1,000 plays across 1,000 pages. Even with an index on user_id, the engine performs 1,000 random seeks. Repeating that work for every user would take weeks.
Option B: clustered storage
The design orders the Listens table by (user_id, listen_time), creating a clustered index on those columns. The clustered versus unclustered page explains the mechanism.
The user's 1,000 plays now occupy one 64 MB page or a small set of neighboring pages. The engine reads them sequentially. The same B+Tree also supports date-specific lookups such as "what did I play on July 12 at 9 PM?" because the key includes listen_time.
Phase 2: online delivery
The batch job stores each summary in a Wrapped table. The online path uses two access patterns.
Wrapped card: hash index on user_id
When a user opens Wrapped, Spotify fetches one precomputed summary. A hash index on user_id provides an expected O(1) lookup.
Recently played: B+Tree on a clustered table
The raw Listens table serves range queries such as "show all songs I played in December." Its B+Tree finds the start of December, then the clustered layout reads the range from contiguous pages. A hash index cannot serve that range scan.
One clustering order per table
A table supports one physical clustering order, so the design must choose which access pattern to favor:
-
Clustering by
(user_id, listen_time)makes Wrapped fast and personal-history deep-dives fast. -
Clustering by
(artist_id, user_id)would make "Top 10 Fans of Taylor Swift" fast instead, but Wrapped becomes unclustered (and therefore slow).
A large system can keep multiple physical copies of Listens, each clustered for a different feature. This design spends storage to reduce random IO at query time.
Takeaway: Wrapped moves aggregation before launch, then serves each card from a hash-indexed per-user summary while clustering turns the worked batch read from 1,000 random IOs into one sequential read.