YeetCode
System Design · Advanced Designs

Design Spotify: Streaming, Playlists, and Discover Weekly at Scale

A system design walkthrough for Spotify — audio delivery, capacity math for 100M DAU, collaborative playlists, Discover Weekly, and Spotify Connect.

9 min readBy Bhavesh Singh
music streamingaudio cdnplaylistsrecommendation systemsspotify connect

This article has an interactive companion

Don't just read it — step through it interactively, one state change at a time.

Practice this topic on YeetCode

Designing Spotify surprises people. Everyone braces for a Netflix-scale bandwidth monster and instead finds a system where the audio egress is almost an afterthought — a 160 kbps track is roughly a hundredth the bitrate of a 4K video. The hard parts live elsewhere: making a track start in under 300 milliseconds, keeping a 10,000-song collaborative playlist consistent while five people edit it, serving a personalized home feed under a 150 ms p99, and letting you pause on your phone and resume on your kitchen speaker without a hiccup.

That inversion is exactly why interviewers like this prompt. It rewards candidates who reason from the workload instead of pattern-matching "streaming media" to a CDN diagram and stopping there. Below is the full walkthrough — requirements, capacity math with real numbers, architecture, data model, and the deep dives that separate a plausible sketch from an operable system.

What are we actually building?

Pin down the functional scope before drawing a single box. A workable Spotify MVP owns five capabilities:

  • Catalog and search — browse and look up tracks, albums, and artists by metadata.
  • Audio playback — stream a chosen track with fast start and instant seek.
  • Library and playlists — save songs, create playlists, and collaborate on them.
  • Discovery — a personalized home feed plus weekly recommendations.
  • Cross-device control — start on one device, transfer to another (Spotify Connect).

The non-functional bounds are where this design earns its keep. Track-start latency should feel instant (target under 300 ms to first audio). Playback must survive network jitter without stutter. Playlist edits need to be durable and eventually consistent across collaborators within a second or two. The home feed wants a p99 under 150 ms. Availability matters more for playback than for, say, the analytics pipeline — you fail those differently, and saying so out loud is half the interview.

One framing to state early: this is not video. Files are small (3–10 MB), bitrates are low (96–320 kbps), and adaptive bitrate switching is a minor concern. The scarce resource is not bandwidth — it is tail latency on track start and seamless transitions between tracks.

Capacity estimation: run the numbers

Assume 600M monthly actives, 100M daily actives, 90 minutes of listening per user per day at 160 kbps. These are the numbers to reason from, and each one is worth challenging out loud.

Daily audio egress. Bytes per user per day = 90 min × 60 s × 160 kbit/s ÷ 8 = ~108 MB. Across 100M DAU that is roughly 10.8 PB/day. Big, but it is served almost entirely from CDN edge caches, not your origin.

Concurrent streams. Average concurrency = 100M × (90 / 1440 minutes in a day) ≈ 6.25M simultaneous streams. Apply a ~3× peak factor for evening prime time and you are near 18–20M concurrent. At 160 kbps each, peak egress is about 3 Tbps — comfortably within what a commercial CDN like Akamai, Fastly, or Cloudflare absorbs.

Catalog storage. 100M tracks × 5 MB ≈ 500 TB per encoding. Multiply by 3–4 bitrate variants plus an Opus/lossless tier and you land around 2–3 PB of source plus encoded audio. For context, that is trivial next to a video catalog — a single reason the whole design leans lighter than Netflix.

The takeaway an interviewer wants: bandwidth is a solved, buyable problem here. Budget your design attention on latency, playlist consistency, and personalization instead.

High-level architecture

Keep the synchronous request path short and push everything else onto durable queues. A first cut:

text
┌────────────┐ Clients ── LB ──────┤ API Gateway├──── Auth └─────┬──────┘ ┌────────────┬───────┼────────────┬─────────────┐ ▼ ▼ ▼ ▼ ▼ Catalog svc Playlist Playback/ Feed/Reco Track-access (metadata) svc Session svc svc (license) │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ Postgres + Cassandra Redis + Cassandra Signed-URL search index + op log pub/sub (precomputed) minting Kafka ──► ETL ──► Columnar warehouse (BigQuery/Druid) for analytics Audio bytes: Client ──► CDN edge ──► (miss) Origin object store

The audio bytes never touch your application servers. The client hits a track-access service that authorizes playback and mints a signed CDN URL, then pulls the actual segments straight from the edge. Metadata, playlists, session state, and recommendations each get their own service with a datastore chosen for its dominant access pattern.

How does audio delivery hit sub-300ms starts?

Encode each track once, then slice it into small byte-range-addressable segments — commonly ~500 KB or a fixed duration — packaged in an Opus or Vorbis container that carries a seek table. Push those segments to a CDN with aggressive edge caching keyed by a track fingerprint, so popular songs live warm at the edge worldwide.

Three client tricks make the start feel instant:

  1. Parallelize the first fetch and the license call. The client requests segment 0 at the same moment it asks the track-access service for authorization, rather than serially.
  2. Start on a small buffer. Playback begins as soon as ~100–300 ms of decoded audio is available, not after the whole file lands.
  3. Prefetch the next track. During the last ~30 seconds of the current song, the client pulls the head of the next one. That is what makes album playback gapless.

Seeking becomes a byte-range GET: the container's seek table maps a timestamp to a byte offset, so jumping to 2:15 is one ranged request, not a re-download.

Data model and the API

The metadata catalog is relational — tracks, albums, artists, and their relationships — fronted by a search index (Elasticsearch) for text queries. Playlists are the interesting model. Do not store a playlist as one big array you rewrite on every edit; that guarantees write conflicts on collaborative lists.

Instead, store playlists as an append-only operation log: each edit is an event (insert, remove, move) carrying a client-generated op-id for idempotency and a fractional position key so concurrent inserts between the same two songs don't collide. Materialize the current order into a cached view in Cassandra or Redis for fast reads; the log itself powers undo and audit.

text
POST /v1/tracks/{id}/play -> { streamUrl, licenseToken, segments } GET /v1/search?q=... -> tracks | albums | artists POST /v1/playlists -> { playlistId } POST /v1/playlists/{id}/ops -> { op: "insert"|"move"|"remove", opId, pos, trackId } GET /v1/home -> personalized sections POST /v1/connect/transfer -> { targetDeviceId } (CAS on activeDeviceId)

The /ops endpoint is idempotent on opId, replicates via Kafka to followers and the search index, and fans the change out to subscribed collaborators over WebSocket. A read of the playlist is a single cached-view lookup.

Deep dive: Discover Weekly under 150ms

The trick to serving personalization fast is to do almost no work at request time. Split it into offline and online halves.

Offline (nightly / weekly Spark jobs): compute per-user candidate pools using collaborative-filtering embeddings and content features, and write each user's bundle to a single row in Cassandra or Bigtable. Discover Weekly specifically runs weekly in batch and produces a finished, stored playlist.

Online (per request): fetch the precomputed bundle plus recent listening events from a feature store, run a lightweight reranker (LambdaMART or a small DNN) inside a ~30 ms budget, and return the sections. Serving Discover Weekly is then literally a key-value lookup — which is precisely why a 150 ms p99 is easy. You never run heavy inference in the hot path.

PathWhen it runsWork per requestLatency
Discover WeeklyWeekly batchKV lookup of stored playlist~single-digit ms
Home feedOnline rerankFetch candidates + light model<30 ms model, <150 ms p99
Candidate generationNightly SparkNone (precomputed)offline

Deep dive: Spotify Connect and offline mode

Cross-device control. Every signed-in device holds a persistent connection (WebSocket or long-lived HTTP/2) to a session service and publishes its capabilities. A per-user playback-state document — current track, position, queue, volume, active device — is the authoritative source, replicated via pub/sub. Any controller issues commands against that document; the active device subscribes to changes and reconciles. Transferring playback is a compare-and-set on activeDeviceId with a monotonically increasing version number, which is what stops two devices from both thinking they're in charge (split-brain).

Offline downloads. The client requests an offline bundle; the backend issues a time-bound license and a manifest of encrypted segments. The client downloads them into an app-private directory with keys in the OS keystore. The license requires periodic online re-validation (say, every 30 days) enforced by client-side DRM — miss the check-in and the files stop playing. Download counts are capped per device and per account to deter sharing, and keys rotate when a subscription tier changes.

Analytics. Listening events are append-heavy (billions per day) and queried as big time-window aggregations — top artists, skip rates, A/B metrics. Those belong in a columnar warehouse (BigQuery, Druid), not your OLTP store, which handles wide scans poorly. Keep the OLTP path thin: durably enqueue events to Kafka, then ETL into the warehouse.

How this shows up in interviews

Interviewers steer toward the boundary between a diagram and a system that survives contact with production. Expect the "why not video?" question early — nail the bitrate/latency inversion and you've set the right frame. Expect a capacity drill: be ready to derive concurrency from DAU × (minutes ÷ 1440) and egress from bitrate, and to observe out loud that it's CDN-serviceable. The collaborative playlist is the classic depth probe — reach for an op-log with fractional indexing, not array rewrites, and explain idempotency via op-ids. Finish strong on Discover Weekly by separating offline candidate generation from a thin online reranker; that offline/online split is the single idea that makes the latency target trivial.

Further learning

Related designs that share these building blocks: Design Discord and Design Slack reuse the WebSocket fan-out and per-user session state; Design Google Drive covers chunked object storage and CDN delivery from another angle; Design Ticketmaster drills the same concurrency and idempotency concerns under contention.

FAQ

Why is designing Spotify easier on bandwidth than designing Netflix?

Audio is roughly two orders of magnitude smaller than video. A 160 kbps track streams about 108 MB per user across 90 minutes, versus gigabytes for a movie, and a 100M-track catalog occupies only 2–3 PB across all encodings. Even at ~3 Tbps peak egress, commercial CDNs absorb it comfortably. The engineering effort moves off bandwidth and onto track-start latency, playlist consistency, and personalization — which is why treating this as "just a CDN problem" is the trap.

How do you keep a large collaborative playlist consistent under concurrent edits?

Model the playlist as an append-only operation log instead of a single array. Each edit is an idempotent event — insert, remove, or move — tagged with a client op-id and a fractional position key, so two people inserting between the same pair of songs get distinct positions rather than a conflict. Edits replicate through Kafka to followers and the search index and fan out to collaborators over WebSocket, while reads hit a cached materialized view. The log doubles as your undo and audit trail.

How does Spotify serve Discover Weekly with a p99 under 150ms?

By precomputing almost everything offline. Nightly Spark jobs build per-user candidate pools from collaborative-filtering embeddings and content features and store them per user in Cassandra or Bigtable, and Discover Weekly itself is generated in a weekly batch as a finished playlist. At request time the service does a key-value lookup for the stored bundle and runs only a light reranker within a ~30 ms budget. No heavy model ever executes in the hot path, so the latency target is straightforward to hold.

How does Spotify Connect avoid two devices fighting over playback?

A single authoritative playback-state document per user holds the current track, position, queue, and active device, replicated via pub/sub. Devices hold persistent connections and subscribe to changes; a controller issues commands against the document. Transferring playback is a compare-and-set on the active-device id guarded by a monotonically increasing version number, so a stale device's write is rejected. That versioned CAS is what prevents split-brain where two devices both believe they own playback.

Why route listening analytics to a columnar store instead of the OLTP database?

Listening events arrive as billions of appends per day, and the questions asked of them — top artists, skip rates, A/B metrics — are aggregations over wide time windows. Row-oriented OLTP stores scan those poorly and get expensive fast. Columnar stores like BigQuery or Druid give large compression ratios and scan speedups on exactly that shape of query. The production path stays thin: durably enqueue events to Kafka, then ETL them into the warehouse, keeping the transactional database focused on live reads and writes.

Make it stick: run this one yourself

Don't just read it — step through it interactively, one state change at a time.

Practice this topic on YeetCode