Design Spotify

MediumMedia & StreamingAudio StreamingCDNCatalogPlaylists

Problem

Design a music streaming service. Users search a catalog of about 100 million tracks, play any track with playback starting in well under a second, and create and share playlists. Plays must be counted accurately because royalties are paid per stream. The service has 500 million monthly users, about 100 million of them active on a given day, and should support offline downloads for paying subscribers.

  • How many concurrent listeners do we expect at the daily peak?
  • Which audio qualities and codecs must we support, and do they differ by subscription tier?
  • What counts as a play for royalty purposes, for example at least 30 seconds of listening?
  • Are licensing rules per country, so some tracks must be hidden in some regions?
  • Do we need personalized recommendations in scope, or only search and playlists?
  • Should playlists support collaborative editing by several users?

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • Search tracks, albums, artists, and playlists.
  • Stream any track with fast start and no stalls on a typical mobile connection.
  • Create, edit, follow, and share playlists.
  • Record every qualifying play for royalty reporting.
  • Download tracks for offline playback on premium accounts.
  • Enforce per-country licensing so unavailable tracks are hidden or greyed out.
  • Resume playback on another device from the same position.

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • 100 million daily active users, peaking near 15 million concurrent streams.
  • Playback start under 300 ms at p95 for cached tracks, and under 1 second otherwise.
  • An average track is 3.5 minutes; at 160 kbps that is about 4 MB, so 100 million tracks at three bitrates is roughly 1.5 PB of audio before replication.
  • Peak egress near 15 million streams x 160 kbps, about 2.4 Tbps, which only a CDN can serve economically.
  • Play counting must be at-least-once on the client and deduplicated on the server, since royalties are money.
  • 99.95% availability for playback; search and playlist editing can degrade briefly.

Audio files are transcoded once into several bitrates, encrypted, split into small chunks, and pushed to a multi-tier CDN, so the backend never sits in the audio path. A set of stateless services handles catalog metadata, search, playlists, and playback authorization, issuing short-lived signed URLs and decryption keys. Clients report play events to a durable event stream that feeds deduplicated royalty counts and analytics.

Loading diagram...

Ingest a track. A label uploads a master file and metadata. The ingestion pipeline validates rights by country, transcodes to Ogg Vorbis or AAC at 96, 160, and 320 kbps, encrypts each file with a per-track key, writes chunks to object storage, and upserts catalog metadata, which is then indexed for search.

Play a track. The client calls POST /playback with the track id. The playback service checks the user's country and tier, then returns a signed CDN URL for the right bitrate and the wrapped decryption key. The client fetches the first chunks from the nearest CDN edge, starts playing, and prefetches the next track in the queue so the transition is gapless.

Count a play. When listening crosses the royalty threshold, the client emits a play event with a client-generated event id. Events are buffered on the device if offline and flushed later. The royalty counter deduplicates by event id, enriches with the rights holder, and writes daily totals to the warehouse.

Edit a playlist. The client sends an edit with the playlist's current revision. The playlist service applies it if the revision matches, bumps the revision, and returns the new state; followers see the change on their next refresh.

Authorizes playback and hands out everything the client needs to fetch audio directly from the CDN.

  • Stateless Java or Go service behind the API gateway.
  • Signed URLs with a short expiry, and a key service backed by an HSM for the per-track content keys.

API

  • POST /playback with { trackId, deviceId, quality } returns { cdnUrl, expiresAt, wrappedKey, bitrate }.
  • POST /offline/licenses with { trackIds, deviceId } returns keys bound to the device, valid for 30 days.
  • PUT /devices/:deviceId/state stores the current track and position for cross-device resume.

Scalability

Each request is a few cache lookups (user tier, track rights, key), so a modest fleet handles tens of thousands of authorizations per second. Rights and keys are cached in memory because they change rarely.

Tradeoffs

Separating authorization from delivery means the backend handles one small request per track while the CDN handles megabytes of audio. Proxying audio through our own servers would allow tighter control but would multiply egress cost and latency. Offline licenses expire so a cancelled subscription stops working within 30 days without remote wipe.

Stores encrypted audio chunks and serves them from edges close to listeners.

  • Object storage such as S3 as the origin.
  • A commercial CDN plus regional caching tiers for popular tracks.

Diagram

Loading diagram...

Scalability

Listening is heavily skewed: a small fraction of tracks accounts for most plays, so edge hit rates above 95% are typical. The regional shield tier collapses edge misses so the origin sees a tiny fraction of traffic. Clients also keep a local cache of recently played tracks, which removes a large share of repeat requests entirely.

Tradeoffs

Short chunks or byte-range requests let playback start after the first few hundred kilobytes and let the client switch bitrate between chunks on a weak network. Full adaptive streaming manifests like HLS add little for audio because a single bitrate rarely stalls; range requests on one file per bitrate keep the client simple.

Serves track, album, and artist metadata and full-text search across them.

  • Catalog in a sharded relational database or Cassandra, fronted by a read cache.
  • Elasticsearch or OpenSearch for search, with typo tolerance and popularity boosting.

Data Model

  • tracks(track_id, title, album_id, artist_ids, duration_ms, isrc, available_countries)
  • albums(album_id, title, artist_id, release_date, cover_url)
  • artists(artist_id, name, genres, follower_count)

Scalability

Catalog reads are extremely cacheable, since metadata changes only on ingestion. The search index is rebuilt continuously from the catalog change log and replicated per region so queries never cross oceans. Popularity signals are refreshed daily into the index as a ranking field.

Tradeoffs

Country availability is stored on the track and applied as a search filter, rather than keeping a separate index per country, which would multiply storage by roughly 180. The filter costs a little query time but keeps one index to maintain.

Stores playlists, their track lists, and followers.

  • Cassandra or DynamoDB for playlist documents.
  • Redis cache for the most followed playlists.

API

  • POST /playlists creates a playlist.
  • PATCH /playlists/:id with { baseRevision, ops: [add, remove, move] } applies edits.
  • GET /playlists/:id?offset=&limit= returns a page of tracks.
  • PUT /playlists/:id/followers follows a playlist.

Data Model

  • playlists(playlist_id, owner_id, name, revision, is_collaborative, updated_at)
  • playlist_tracks(playlist_id, position, track_id, added_by, added_at)

Scalability

Most playlists are small and edited rarely. Editorial playlists with millions of followers are read heavy and served from cache, invalidated on each revision bump.

Tradeoffs

Optimistic concurrency with a revision number is enough for collaborative playlists, because conflicting edits are rare and the client can rebase simple add, remove, and move operations. A real-time CRDT would handle concurrent edits more gracefully but is unnecessary for this edit rate.

Collects play events from every client and produces exact, auditable play counts per track per country per day.

  • Kafka for the event stream, with events stored for 7 days.
  • Flink or Spark for deduplication and aggregation, writing to a warehouse such as BigQuery or Snowflake.

Data Model

  • play_events(event_id, user_id, track_id, country, ms_played, started_at, offline)
  • daily_plays(track_id, country, day, play_count, rights_holder_id)

Scalability

At roughly 3 billion plays per day (about 35,000 events per second on average), the stream is partitioned by user id. Deduplication keeps a window of seen event ids per partition, and offline events arriving days late are accepted into the correct day's partition.

Tradeoffs

Royalty counts are computed in batch from the raw event log rather than from live counters, so any bug can be fixed and the day recomputed for audit. Live counters on the hot path would be cheaper to read but impossible to correct after the fact.

Related questions