SYSTEM DESIGN · DESIGN PROBLEMS · MEDIA AND STORAGE · MEDIUM · 18 OF 41

Design Spotify: Audio Delivery, Playlists, Offline Play and Accurate Play Counts

How to design a music streaming service like Spotify: requirements, listening and catalog math, why audio is a small-file problem unlike video, CDN delivery with a client cache and gapless prefetch, per-country rights checks, a versioned playlist model for collaborative edits, offline downloads with licence expiry, play counts accurate enough for royalties, batch recommendations, and the trade-offs.

Reported atWalmart Global Tech

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
1 / 18

Press play to hear the narration, or step through at your own pace.

Notes

Listen

Requirements. Users search the catalog and play any track. They create playlists, follow others' playlists and can edit some together. Premium users download music for offline play. Every qualifying play is counted, because royalties depend on it. And users get personalised weekly mixes. Podcasts, lyrics, the social activity feed and ad serving are out of scope.

Qualities. A track should start in about two hundred milliseconds, and the next one should already be loaded, so there's no gap between songs. Playlist edits should show up on your other devices within seconds. Play counts must be accurate: nothing lost, nothing double counted, because they turn into payments. Licences differ by country, so a track available in one market may be blocked in another. And music should keep playing even if parts of the backend have problems.

Functional requirements
  • in scope:
  • • search and play any track
  • • create, follow and share
  • playlists; collaborative
  • • download for offline play
  • (premium)
  • • count plays for royalties
  • • personalised weekly mixes
  • out: podcasts, lyrics, social
  • feed, ads serving
Non-functional requirements
  • track starts in ~200 ms
  • gapless: next track ready
  • before this one ends
  • playlists sync in seconds
  • across devices
  • play counts: no loss, no
  • double count (money)
  • respect licences per country
  • available: music keeps playing
  • even if services degrade

Ask scale

Assume two hundred fifty million daily listeners at two and a half hours each: over six hundred million hours a day. At about three and a half minutes per track, that's around ten billion plays a day, roughly a hundred thousand track starts per second. Average concurrent listeners are about twenty-six million, double at peak. At a hundred sixty kilobits a second, peak egress is around eight terabits. Big, but an order of magnitude below video.

Catalog: around a hundred million tracks. Store each at three quality levels, about fifteen megabytes in total per track, so the whole catalog is about one and a half petabytes. The most popular million tracks, which get most of the plays, are only about fifteen terabytes and fit in CDN edge caches easily. The larger data stream is actually play events: ten billion a day at a couple of hundred bytes each is two terabytes daily.

Traffic
DAU                   250M
listening/user/day    2.5 h
  = 625M hours/day
avg track             3.5 min
  = ~10B plays/day
  / 100,000 s        = 100K/s

avg concurrent = 625M / 24
                      = ~26M
peak x2 at 160 kbps
  = 52M x 160 kbps   = ~8 Tbps
Catalog and storage
tracks                 ~100M
avg length             3.5 min
qualities 96 + 160 + 320 kbps
  = 576 kbps x 210 s
  = ~15 MB per track, all
100M x 15 MB         = ~1.5 PB

popular 1M tracks
  = 15 TB -> fits in CDN edges
play events 10B x 200 B
  = 2 TB/day

Define API

The API. Search by text and type. Asking to play a track checks rights and returns a short-lived signed CDN URL plus the decryption key and quality. Playlists are read by revision, and edited by sending operations like add, move and remove along with the revision you started from. Plays are reported in batches, each with a unique play ID generated on the device, which is what lets the server deduplicate retries.

Core API
GET  /v1/search?q=..&type=track
GET  /v1/tracks/{id}/play
  -> { cdnUrl (signed, 1 h),
       key, quality }
GET  /v1/playlists/{id}?rev=..
POST /v1/playlists/{id}/ops
  { baseRev, ops:[add|move|rm] }
  -> { newRev }
POST /v1/plays  (batched)
  [{ playId, trackId, ms, ts }]

Data model

Data model. The catalog is read-mostly and heavily cached: tracks, albums, artists and the file keys for each quality. Rights rows say whether a track is allowed in a country and for what dates. A playlist has an owner, a revision number and collaborators, and its entries are ordered tracks. Play records are an append-only log keyed by the device-generated play ID. Playlists suit a wide-column store; plays go to a durable stream and then a warehouse.

Records
track   (trackId, albumId,
         artists[], duration,
         files{96,160,320})
rights  (trackId, country,
         allowed, from, until)
playlist(playlistId, ownerId,
         rev, collaborators[])
entry   (playlistId, pos, trackId)
play    (playId PK, userId,
         trackId, msPlayed, ts)

High-level design

The play path. The app searches, then asks to play a track. The catalog service checks the rights service: is this track licensed in the user's country right now? If yes, it returns a signed CDN URL that expires in an hour and the key to decrypt the file. The app streams from the nearest CDN edge. A miss falls through to audio storage, but popular tracks are almost always at the edge.

Now the rest. Playlist edits go to the playlist service and its store. Play reports go into a durable event stream. Two consumers read it: a royalty pipeline that deduplicates and counts qualifying plays, and the recommendation system, which uses listening history to build weekly mixes in a batch job and writes them back as ordinary playlists. That's a nice touch: recommendations reuse the playlist machinery.

Deep dive: delivery

Deep dive one: delivery. Unlike video, a track is a single small file, so there's no need for a segment ladder. Fetch it with range requests so the first seconds arrive immediately and playback starts. About thirty seconds before the end, prefetch the next track in the queue so playback is gapless. Keep a client-side cache of recently played tracks, because people replay favourites constantly, and every replay from cache is free bandwidth.

Offline downloads use the same encrypted files, stored on the device. The decryption key comes with a licence that expires, say after thirty days, so the app must go online now and then to renew it. If the subscription lapses, keys stop renewing and the files become unplayable. Plays made offline are queued on the device with their play IDs and uploaded later, so they still count toward royalties exactly once.

Small files, played in order
  • a track is one file, ~3-8 MB
  • fetch with range requests
  • so the first seconds arrive
  • first
  • prefetch next track at ~30 s
  • before the end: gapless
  • client cache (1-10 GB):
  • replays cost nothing
  • quality by network:
  • 96, 160 or 320 kbps
Offline downloads
  • download = same encrypted file
  • stored on the device
  • key stored with a licence that
  • expires (e.g. 30 days)
  • app must check in online
  • periodically to renew
  • subscription lapses: keys
  • stop renewing, files dead
  • offline plays queued, sent
  • later with their play IDs

Deep dive: playlists

Deep dive two: playlists. Treat a playlist like a small shared document: a snapshot plus a log of operations, each with a revision number. A client sends its operations with the revision it started from. If someone else edited in between, the server rebases the operations onto the newer revision. Adds and removes rarely conflict, and a move of a track that was deleted is just dropped. The reply includes the new revision and any operations the client missed, so every device converges.

What about a chart playlist followed by tens of millions of people? Don't push its edits to anyone. A follower's library just stores the playlist ID. Each revision of the playlist is immutable, so cache it aggressively. Clients ask a cheap, cacheable question, what's the latest revision, and fetch the new one only when it changes. It's the pull model again, which suits content that a few people write and millions read.

Collaborative playlists
  • store: snapshot + op log
  • (rev, op, author, ts)
  • client sends ops + baseRev
  • server: if base is old,
  • rebase ops onto newer revs
  • (add/remove rarely clash)
  • reply: new rev + missed ops
  • huge playlists (10K tracks):
  • page entries, cache by rev
Followed playlists at scale
  • a top chart playlist has
  • tens of millions of followers
  • do not push edits to them
  • followers read by revision:
  • cache each rev at CDN or
  • in a key-value cache
  • clients poll "latest rev?"
  • cheap, cacheable
  • my library = list of IDs

Deep dive: play counts

Deep dive three: play counts. Because counts become money, aim for effectively exactly once. The device creates a unique play ID for each play. Industry practice is that a stream counts after about thirty seconds of listening. The app retries uploads until acknowledged, which means duplicates, so the consumer deduplicates on play ID. A real-time count feeds dashboards, but payouts come from a daily batch recount of the raw log, which also filters fraud like bot accounts looping one track.

Counting for royalties
  • device makes playId (UUID)
  • rule: counts after ~30 s
  • of listening
  • stream is durable; producer
  • retries until acked
  • consumer dedupes on playId
  • (seen-set per day)
  • daily batch recount from the
  • raw log = the source of
  • truth for payouts
  • fraud: bots, loops, farms

Trade-offs

Failures. If the catalog service is slow, the app's queue already has signed URLs and keys for the next few tracks, so music keeps playing. If a CDN edge fails, retry another edge, or even another CDN provider. A playlist conflict is rebased, and an add is never lost. If the event pipeline backs up, counts lag but nothing is lost. Watch track start latency, playback stalls, and the duplicate rate in the counting pipeline as a health signal.

The trade-offs. Client caching and prefetching spend device storage and some wasted bytes to make playback instant. Signed URLs add a little complexity but stop people from hotlinking audio. Operation logs with rebasing are more work than overwriting the whole playlist, but they make collaboration safe. And batch recounting delays the final numbers in exchange for payouts you can defend. At ten times the scale: more edge capacity and bigger client caches.

Failures and monitoring
  • catalog slow: cached rights
  • and URLs keep queue playing
  • CDN edge down: retry other
  • edge or CDN provider
  • playlist conflict: rebase,
  • never lose an add
  • event stream backlog: counts
  • lag, nothing lost
  • watch: start latency, skips
  • from stalls, dedupe rate
Trade-offs to name
  • client cache + prefetch:
  • device storage for speed
  • signed URLs: some CDN
  • cacheability for control
  • ops + rebase: complexity for
  • safe collaboration
  • batch recount: delay for
  • correct payouts
  • 10x: more edge, smarter cache

Say it like this

The summary. About ten billion plays a day, under ten terabits at peak. Playing a track checks country rights, then returns a signed CDN URL; the app range-fetches, prefetches the next track and caches favourites. Offline uses encrypted files with expiring licences. Playlists are snapshots plus operation logs, rebased when edits collide, and popular ones are cached by revision. Plays carry a device-generated ID, count after thirty seconds, are deduplicated, and are recounted daily for payouts.

The 60-second answer
  • ~10B plays/day, ~8 Tbps peak
  • rights check -> signed CDN URL
  • range fetch, prefetch next,
  • client cache, 3 qualities
  • offline: encrypted file +
  • expiring licence
  • playlists: snapshot + op log,
  • rebase on stale baseRev
  • plays: playId, 30 s rule,
  • dedupe, daily recount

Interview questions

How is audio streaming different from video streaming?

Tracks are small single files of a few megabytes played back to back, so you can fetch whole files with range requests, prefetch the next track and cache favourites on the device. Video needs segmented adaptive bitrate streaming because files are huge and bandwidth varies during playback.

How do you make playback gapless?

Start fetching the next track in the queue about thirty seconds before the current one ends, including its rights check and signed URL. When the current track finishes, the next one is already buffered locally.

How do you handle two people editing a collaborative playlist at once?

Store the playlist as a snapshot plus an operation log with revision numbers, and have clients send operations with the revision they started from. The server rebases stale operations onto the latest revision and returns the new revision with any operations the client missed.

How do you count plays accurately enough for royalties?

Each play gets a unique ID on the device, is reported through a durable stream with retries, and is deduplicated by that ID downstream. A daily batch recount over the raw log applies the qualifying rules and fraud filters and is the source of truth for payouts.

How do offline downloads stay protected?

Downloaded files stay encrypted, and the key is tied to a licence that expires unless the app checks in online to renew it. If the subscription ends, the licence is not renewed and the files can no longer be decrypted.

How do you enforce that a track is only playable in licensed countries?

A rights service stores availability by track, country and date range, and the play endpoint checks it before issuing a short-lived signed CDN URL. Because URLs expire quickly, a revoked licence takes effect within the hour.

How would you serve a playlist with tens of millions of followers?

Never fan out its edits; followers store only the playlist ID. Each revision is immutable and cached, and clients cheaply check for the latest revision and fetch it only when it changes.

How much storage does the catalog need?

About a hundred million tracks at three quality levels is roughly fifteen megabytes per track, about one and a half petabytes in total. The most-played million tracks are only about fifteen terabytes, small enough to keep at CDN edges.