System Design · Interview Deep Dive

Design Spotify: Music Streaming System Design

From a master recording to the first second of audio on a phone in under a few hundred milliseconds, plus the playlists, downloads, recommendations, search and royalty counting around it.

✍️ Pranjal Jain, Ex-Microsoft · IIT Kanpur 📅 10 October 2026 ⏱ 17 min read
Sound waves from a record travelling through CDN towers to phones and headphones with playlist cards floating nearby
TL;DR
  • Audio is encoded once per quality tier at ingestion, stored in object storage and served from CDNs with HTTP range requests. Device caching and prefetching the next track make playback feel instant.
  • The catalogue is small (petabytes); bandwidth is the real cost (terabits per second at peak), so cache hit rate is the key metric.
  • Playlists: metadata row + ordered entries with fractional positions; version numbers for sync and collaborative edits.
  • Offline downloads: encrypted files on device, keys bound to account/device, periodic online licence checks.
  • Royalties are an event pipeline problem: play events → dedup → 30-second rule → fraud filtering → per-track counts → pro-rata payouts, reconciled in batch.

Music streaming looks like a smaller version of video streaming, and many candidates design it that way: upload, transcode, CDN, done. Interviewers at Spotify, JioSaavn, Gaana, Amazon Music or YouTube Music then push on the parts that are unique to music: instant playback on flaky mobile networks, offline listening on a train with no signal, playlists edited by five friends at once, recommendations that drive most listening, and paying the right rights holders for billions of plays.

This guide designs a Spotify-like service with those parts given real depth. Where Spotify has published details (bitrates, the 30-day offline rule, stream thresholds) they are mentioned with the caveat that such details change; everything else is a sound reference design.

Step 1: Requirements

Functional

  • Play any track in the catalogue on mobile, desktop, web and connected devices, with seek, skip and gapless playback.
  • Browse artists and albums; search tracks, artists, albums, playlists and podcasts.
  • Create, edit, follow and share playlists, including collaborative ones.
  • Offline downloads for premium users.
  • Recommendations: home feed, radio, personalised playlists, autoplay.
  • Count plays for royalties and artist analytics.
  • Free tier with ads and quality limits; premium tier.

Non-functional

  • Low start latency: first audio within a few hundred milliseconds on good networks; no rebuffering mid-track.
  • Works on 3G/4G in tier-2 Indian cities and on metro rides with intermittent signal.
  • High availability for playback; the catalogue and playlists can be slightly stale but playback must not stop.
  • Accurate and auditable stream counts, because they drive payments.
  • Licensing: availability differs by country and changes over time.

Step 2: Capacity Estimates

Assumptions for the interview (Spotify reports several hundred million monthly users; these are round numbers for math, not its real figures):

QuantityAssumption / mathResult
Daily active usersAssumption200 million
Listening time2 hours per DAU per day400M listening hours/day
Average concurrent listeners400M h / 24 h~16.7 million
Peak concurrent listeners2x average (evening)~33 million
Peak egress bandwidth33M × 160 kbps (minus cache hits on devices)~5.3 Tbps upper bound
Track starts~3.5-minute tracks: 400M h × 60 / 3.5~6.9 billion/day ≈ 80,000/s
CatalogueAssume 100 million tracks—
Bytes per track, one bitrate160 kbps = 20 KB/s × 210 s~4.2 MB
All encodings per trackSeveral lossy tiers + lossless~40 MB
Total audio storage100M × 40 MB~4 PB (×3 for replication / regions)
Play eventsStart, progress, end per trackTens of billions of events/day

The conclusions are clear. Storage of a few petabytes is manageable. Egress of several terabits per second is the dominant cost, so everything about delivery is designed to maximise CDN and device cache hits. Track starts at ~80k/s drive the metadata and licensing checks. And tens of billions of play events per day make the royalty pipeline a serious streaming system. For the method behind these numbers, see back-of-the-envelope estimation.

Step 3: API Design

HTTP API
GET  /v1/tracks/{track_id}/playback?quality=high&format=ogg
     -> { "cdn_urls": ["https://audio-cdn-1.example/ab/cd/<file_id>?sig=...&exp=..."],
          "file_id": "...", "codec": "vorbis", "bitrate": 160,
          "key_ref": "...", "duration_ms": 214000, "normalization_gain_db": -6.2 }

GET  /v1/me/playlists                       -> playlists with versions
GET  /v1/playlists/{id}?since_version=41     -> changes since version 41, or full list
POST /v1/playlists/{id}/tracks
     { "track_ids": [...], "after_item_id": "it_88", "base_version": 41 }
     -> { "version": 42 }  or 409 Conflict (client rebases and retries)

GET  /v1/search?q=tum+hi+ho&types=track,artist&market=IN&limit=20
GET  /v1/me/recommendations/home

POST /v1/events:batch     [ {type: "play_progress", track_id, ms_played, ...}, ... ]

The playback endpoint is a metadata call, not an audio stream: it checks entitlement and licensing for the user's country, then returns signed, short-lived CDN URLs. Audio bytes flow directly from the CDN. Separating the two keeps app servers out of the multi-terabit data path.

Step 4: High-Level Architecture

INGESTION (labels, distributors) ┌─────────────┐ ┌────────────────┐ ┌──────────────────┐ │ Ingest API │──►│ Encoding farm │──►│ Object storage │──► CDN origin │ masters + │ │ (Vorbis/AAC/ │ │ audio files by │ │ metadata │ │ FLAC tiers) │ │ file_id │ └─────┬───────┘ └────────────────┘ └──────────────────┘ ▼ │ ┌─────────────┐ ▼ │ Catalog DB │──► search indexer ┌──────────────────┐ │ + rights / │ │ CDN edges (ISP │ │ territories │ │ POPs in India) │ └─────────────┘ └────────▲─────────┘ │ range GETs CLIENT ── API GW ──┬─► Playback svc (entitlement, signed URLs) │ ├─► Playlist svc ──► playlist store (wide-column) │ ├─► Search svc ──► search cluster │ ├─► Reco svc ──► feature store + models │ └─► Events gateway ─► Kafka ─┬─► royalty pipeline │ ├─► reco training data └─ local cache: audio, metadata, downloads └─► analytics lake

Deep Dive 1: Audio Ingestion and Encoding

Labels and distributors deliver lossless masters (typically WAV or FLAC) plus metadata (ISRC codes, credits, release dates, territories, rights holders). The pipeline:

  1. Validate files and metadata; reject duplicates by ISRC and audio fingerprint.
  2. Encode into several tiers. Spotify has historically used Ogg Vorbis at roughly 96, 160 and 320 kbps, AAC for some platforms, and added lossless FLAC for premium users in 2025. Encoding is embarrassingly parallel and runs on a batch farm.
  3. Loudness analysis: compute normalisation gain so tracks play at consistent volume.
  4. Feature extraction: audio embeddings, tempo, key and energy for recommendations.
  5. Store each encoded file in object storage under an opaque file_id; the catalogue maps track_id to the list of files per codec and bitrate.
  6. Publish to the catalogue and search index only when the release date and territory rules allow.

Encoding once at ingestion, rather than on the fly, is the right call: the catalogue changes slowly, files are small, and on-the-fly transcoding at 80,000 starts per second would be absurd. The same reasoning applies to video in YouTube / Netflix system design, where files are far larger and adaptive bitrate per segment matters more.

Deep Dive 2: Playback, CDN and Prefetching

The playback sequence

  1. Check local cache. If the track (or its first chunk) is cached on the device, playback starts immediately with zero network.
  2. Get playback info. One API call returns signed CDN URLs and decryption key references, after entitlement and territory checks.
  3. Range request the start. The player fetches the first few hundred KB with an HTTP Range header and starts playing once a small buffer is filled.
  4. Stream the rest in further range requests, keeping a buffer of tens of seconds ahead.
  5. Prefetch the next track when the current one nears its end (next in queue, playlist or autoplay), so the transition is gapless.

Why music caches so well

Listening follows a steep popularity curve: a small fraction of tracks accounts for a large share of plays, and new releases from top artists spike on Friday. A 4 MB file served to millions of listeners is ideal CDN content. Strategies:

  • CDN edges inside ISPs (Jio, Airtel and others in India) keep traffic local and cheaper.
  • Pre-warm edges with big Friday releases before midnight drops.
  • Device cache: the app keeps a cache of recently played tracks (hundreds of MB to a few GB, user-configurable), so repeat listens, which are extremely common in music, cost no bandwidth.
  • Adaptive quality: on poor networks pick a lower bitrate for the next track; switching mid-track is rarely worth it for audio.
💡
A historical detail interviewers enjoyEarly Spotify desktop clients used a peer-to-peer network to fetch parts of tracks from other listeners, reducing server bandwidth. Spotify phased P2P out around 2014 as its own server and CDN capacity grew. It is a good example of a trade-off that changes as scale and costs change.

Deep Dive 3: Playlists

Playlists are the core user-generated data: libraries can hold thousands of playlists, some playlists have tens of thousands of tracks, and popular editorial playlists have millions of followers.

Data model

TableKeyColumns
playlistsplaylist_idowner_id, name, description, image, collaborative flag, public flag, version, updated_at
playlist_items(playlist_id, position_key)item_id, track_id, added_by, added_at
user_library(user_id, sort_key)playlist_id or album_id, folder, pinned
playlist_followers(playlist_id, user_id)followed_at (counts maintained asynchronously)

Ordering is the interesting part. Storing integer positions (1, 2, 3…) means inserting at position 2 renumbers every later row. Use a fractional index (a sortable string key): inserting between keys "a" and "b" generates "aV", which sorts in between. Moving a track updates one row, and appending to a 10,000-track playlist never touches the other 9,999.

Sync and collaborative edits

  • Each change bumps the playlist version and appends to a change log. Clients sync with "give me changes since version 41".
  • Writes include base_version. If the server is at a newer version, it returns 409; the client fetches the delta, re-applies its intent (for example "add track X after item it_88") and retries. Because edits are expressed relative to item IDs, not numeric positions, most concurrent edits merge cleanly.
  • For richer real-time collaboration, the same problem is solved with OT or CRDTs, discussed in Google Docs: OT vs CRDT. For playlists, version-based rebase is usually enough.

Storage choice

A wide-column store (Cassandra-like) fits: partition by playlist_id, cluster by position_key, so reading a playlist is one sequential partition scan. Very large playlists can be split into pages. Popular editorial playlists are read-heavy and change rarely, so serve them from a cache keyed by (playlist_id, version).

Deep Dive 4: Offline Downloads and DRM

Offline is a premium feature with licensing obligations: labels allow downloads only for paying subscribers and only inside the app.

  1. Download. The client fetches the encrypted file for the chosen quality, the same file the CDN serves for streaming. Files are encrypted once at ingestion with per-file content keys.
  2. License. The client requests the content keys from a licence service. Keys are wrapped with a device-specific key stored in the platform's secure storage (Android Keystore, iOS Keychain), so copying files to another device is useless.
  3. Limits. Spotify documents limits such as up to 10,000 tracks on each of up to 5 devices, and requires the app to go online at least once every 30 days. The licence carries an expiry; when it lapses, offline playback stops until the app re-validates the subscription.
  4. Revocation. If a track is removed from the catalogue or a subscription ends, the next online check deletes the keys or files.
  5. Offline play events are stored locally and uploaded later with their original timestamps, so royalty counting still sees them (this is one of the "late events" sources for the royalty pipeline).

Download management on the client matters too: resumeable downloads via range requests, Wi-Fi-only by default, storage-aware eviction, and re-download on quality change. Indian users often have low-storage phones, so showing download size and allowing SD-card storage is a real product consideration.

Music search is short-query, typo-heavy and popularity-driven. The query "arjit" should find Arijit Singh, "tum hi ho" should find the song whether its title is stored in Latin script or Devanagari, and "lofi" should surface popular playlists, not a 2009 obscure track that happens to contain the word.

  • Index: tracks, artists, albums, playlists, podcasts in a search cluster (Elasticsearch/OpenSearch or a custom engine). The inverted index basics are in Elasticsearch explained.
  • Typo tolerance: edit-distance fuzzy matching and phonetic analysers, essential for artist names.
  • Transliteration: index Indian-language titles in both native script and common romanisations so Hinglish queries match.
  • Prefix search for autocomplete: edge n-grams or a dedicated suggestion structure updated from popular queries; the dedicated design is search autocomplete system design.
  • Ranking: text relevance blended with popularity in the user's market, freshness and personal affinity (artists the user already plays rank higher).
  • Territory filtering: hide tracks not licensed in the user's country.

Deep Dive 6: Recommendations

Recommendations drive a large share of listening on modern streaming platforms through home feed shelves, radio, autoplay and personalised playlists such as Spotify's Discover Weekly. The architecture is the standard two-stage funnel:

StagePurposeTechniques
Candidate generationFrom 100M tracks to a few thousandCollaborative filtering (matrix factorisation or embeddings from co-listening and co-playlisting), content similarity from audio embeddings, artist graph, trending in your city
RankingOrder candidates for this user nowModel predicting listen-through, save or skip; features: user history, time of day, device, recent session
Re-ranking / policyMake the list good as a wholeDiversity, no repeated artist back to back, freshness, new-artist exposure, explicit-content settings

Two kinds of pipelines run side by side. Batch jobs (daily or weekly) compute embeddings and personalised playlists for every user, writing results to a key-value store for fast reads. Real-time session models adjust radio and autoplay based on the last few skips and plays, using features streamed from the event pipeline. Cold start is handled with content features for new tracks and onboarding choices (pick some artists) for new users. The general recommender architecture is covered in depth in recommendation system design.

Interestingly, playlists are a goldmine for collaborative filtering: millions of human-curated playlists say "these songs belong together", which is often a stronger signal than raw listening.

Deep Dive 7: Counting Streams for Royalties

This is the part most candidates skip and the part that matters most to the business. Payments to labels, publishers and artists are computed from stream counts, so the pipeline must be complete, deduplicated, fraud-resistant and auditable.

Pipeline

  1. Emit events. The player sends play_start, periodic play_progress and play_end events with a unique event_id, ms_played, track, user, device, country and the reason the track ended (skip, completed). Offline events are buffered and uploaded later.
  2. Ingest durably. An events gateway writes them to Kafka; raw events are archived to a data lake as the source of truth.
  3. Qualify. A play counts as a stream once it passes 30 seconds, the commonly used industry threshold.
  4. Deduplicate by event_id (retries from flaky mobile networks are common).
  5. Filter artificial streams. Bot farms and stream-manipulation services exist; signals include accounts playing the same tracks on loop 24 hours a day, many accounts from one device or IP range, and plays just past 30 seconds. Flagged plays are excluded and can be removed retroactively.
  6. Aggregate per track, market, product tier and period in a batch job (stream processing powers real-time artist dashboards, but payment numbers come from the reconciled batch).
  7. Compute payouts and produce statements per rights holder.

The pro-rata model

Spotify describes its model as streamshare: revenue for a market and period goes into a pool, and each rights holder receives the share of the pool equal to its share of qualifying streams. Since 2024, Spotify has also required a track to reach a minimum number of streams (1,000 in the previous 12 months) before it generates recorded royalties. Royalty rules differ across services and change, so confirm current details if you build on them.

Python
from collections import defaultdict

MIN_MS = 30_000            # a play counts as a stream after 30 seconds
MIN_TRACK_STREAMS = 1_000  # eligibility threshold (simplified to one period)


def count_streams(events, fraud_users):
    """events: (event_id, user_id, track_id, ms_played). Returns qualified streams per track."""
    seen, per_track = set(), defaultdict(int)
    for event_id, user, track, ms in events:
        if event_id in seen or user in fraud_users or ms < MIN_MS:
            continue
        seen.add(event_id)
        per_track[track] += 1
    return dict(per_track)


def pro_rata(per_track, track_owner, pool):
    eligible = {t: n for t, n in per_track.items() if n >= MIN_TRACK_STREAMS}
    total = sum(eligible.values())
    payout = defaultdict(float)
    for t, n in eligible.items():
        payout[track_owner[t]] += pool * n / total
    return dict(payout)


events = [("e1", "u1", "t1", 45_000), ("e1", "u1", "t1", 45_000),   # duplicate delivery
          ("e2", "u2", "t1", 12_000),                               # skipped before 30 s
          ("e3", "bot9", "t2", 31_000)]                             # flagged account
print(count_streams(events, fraud_users={"bot9"}))                  # {'t1': 1}

per_track = {"t1": 300_000, "t2": 900_000, "t3": 800_000, "t4": 600}
owners = {"t1": "label_A", "t2": "label_B", "t3": "label_A", "t4": "indie_C"}
print(pro_rata(per_track, owners, pool=1_000_000))   # {'label_A': 550000.0, 'label_B': 450000.0}
Java
import java.util.*;

public class Royalties {
    record Play(String eventId, String userId, String trackId, long msPlayed) {}

    static final long MIN_MS = 30_000;
    static final long MIN_TRACK_STREAMS = 1_000;

    static Map<String, Long> countStreams(List<Play> plays, Set<String> fraudUsers) {
        Set<String> seen = new HashSet<>();
        Map<String, Long> perTrack = new HashMap<>();
        for (Play p : plays) {
            if (fraudUsers.contains(p.userId()) || p.msPlayed() < MIN_MS) continue;
            if (!seen.add(p.eventId())) continue;           // duplicate event
            perTrack.merge(p.trackId(), 1L, Long::sum);
        }
        return perTrack;
    }

    static Map<String, Double> proRata(Map<String, Long> perTrack,
                                       Map<String, String> owner, double pool) {
        long total = perTrack.values().stream().filter(n -> n >= MIN_TRACK_STREAMS)
                             .mapToLong(Long::longValue).sum();
        Map<String, Double> payout = new TreeMap<>();
        perTrack.forEach((track, n) -> {
            if (n >= MIN_TRACK_STREAMS)
                payout.merge(owner.get(track), pool * n / total, Double::sum);
        });
        return payout;
    }

    public static void main(String[] args) {
        Map<String, Long> perTrack = Map.of("t1", 300_000L, "t2", 900_000L, "t3", 800_000L, "t4", 600L);
        Map<String, String> owner = Map.of("t1", "label_A", "t2", "label_B", "t3", "label_A", "t4", "indie_C");
        System.out.println(proRata(perTrack, owner, 1_000_000));  // {label_A=550000.0, label_B=450000.0}
    }
}

Dry run

  • Event e1 arrives twice (a mobile retry): counted once. e2 played 12 seconds: not a stream. e3 is from a flagged bot account: excluded. Result: t1 = 1 stream.
  • For the payout: t4 has 600 streams, below the 1,000 threshold, so it is excluded from both numerator and denominator. Eligible total = 300k + 900k + 800k = 2,000,000.
  • label_A owns t1 and t3: (300k + 800k) / 2M = 55% of a pool of 10 lakh = ₹5.5 lakh. label_B: 900k / 2M = 45% = ₹4.5 lakh.

In production this logic runs in a batch engine over billions of rows, with money in integer minor units (paise or micro-units), not floating point, and with rounding rules defined contractually. The streaming side (late offline events, dedup, exactly-once aggregation) is the same problem as counting ad clicks, covered in ad click aggregation system design.

Bottlenecks and Trade-offs

DecisionTrade-off
Pre-encode all tiers vs transcode on demandMore storage (cheap) vs huge CPU at play time (expensive). Pre-encode wins.
Bigger device cacheLess bandwidth and instant replays vs phone storage complaints
Aggressive prefetchGapless playback vs wasted data if the user skips (respect data-saver mode)
Real-time vs batch royalty countsFresh artist dashboards vs auditable payments; use both with batch as truth
Strong consistency for playlistsFewer surprises vs higher latency; version-based optimistic concurrency is enough
Long offline licence windowBetter travel experience vs weaker subscription enforcement

Hot spots to call out: a superstar album drop at midnight (pre-warm CDN, cache catalogue and artist pages, protect the playback metadata service with caching and rate limits), and year-end "Wrapped" style campaigns, which are precomputed in batch over weeks and served from a key-value store, never computed live.

What Interviewers Look For

Signal 1

Bandwidth-first thinking

You calculate Tbps egress and design for CDN and device cache hits, not just storage.

Signal 2

Music-specific UX

Instant start, prefetching, gapless playback, offline with DRM and licence expiry.

Signal 3

Clean data models

Playlist ordering that does not renumber rows; versioned sync for collaborative edits.

Signal 4

Money pipeline

30-second rule, dedup, fraud filtering, late offline events, batch reconciliation for payouts.

Common mistakes

  • Streaming audio through application servers instead of signed CDN URLs.
  • Transcoding on every play.
  • Ignoring licensing and territory checks in the playback path.
  • Integer playlist positions that require renumbering on every insert.
  • Counting every play_start as a stream, with no dedup or fraud filtering.
  • Forgetting that offline plays arrive days later and must still be counted.

Follow-ups to prepare

  • "How would you add podcasts?" Longer files, resume position sync, episode feeds via RSS ingestion, different royalty model (ads and subscriptions).
  • "How would you build lyrics in sync?" Time-coded lyrics files fetched with the track metadata and cached like audio.
  • "How do you support Spotify Connect (play on a speaker from your phone)?" A device registry and a per-user playback state service that devices subscribe to over a persistent connection; the phone is only a remote control.

Frequently Asked Questions

How does Spotify stream music so fast?▾
Tracks are pre-encoded at several bitrates and split so the player can fetch the first bytes with HTTP range requests from a nearby CDN edge. The app also caches recently played and likely next tracks on the device and prefetches the next song before the current one ends, so most playback starts almost instantly.
Is music streaming system design the same as YouTube or Netflix design?▾
The building blocks overlap, such as transcoding, object storage and CDNs, but audio files are small, so the catalogue fits comfortably on CDNs and devices. Music design leans more on prefetching, offline downloads, playlists, licensing restrictions and per-stream royalty accounting than on adaptive video streaming.
How are offline downloads protected on Spotify?▾
Downloaded tracks are stored encrypted on the device and can only be decrypted by the app with keys tied to the account and device. The app must reconnect periodically, Spotify says at least once every 30 days, so subscriptions and licences can be re-validated.
How does Spotify count streams for royalties?▾
The player sends play events, and a play is generally counted as a stream once it passes 30 seconds. Events are deduplicated, filtered for artificial streaming and aggregated per track, market and period, then revenue is split among rights holders in proportion to their share of streams.
How do music recommendations work?▾
A typical system combines collaborative filtering on listening history, content signals from audio analysis and text such as playlist titles, and context such as time of day. Candidates from these sources are ranked by a model that predicts the chance of a listen or save, with rules for diversity and freshness.
How should a playlist be stored?▾
Store playlist metadata in one record and the tracks as ordered entries keyed by playlist ID with a sortable position, such as a fractional index, so inserting or moving one track updates one row. Keep a version number for sync and optimistic concurrency on collaborative playlists.
Which database fits a music streaming service?▾
Different data needs different stores: a relational or strongly consistent store for users and subscriptions, a wide-column or key-value store for playlists and listening history, a search engine for the catalogue, object storage plus CDNs for audio, and a data lake for events and analytics.

Master System Design Interviews

Prepflix's system design course covers media systems like Spotify, YouTube and Hotstar end to end, with mock interviews that probe CDN, storage and analytics trade-offs.

Explore the Prepflix Course →
Pranjal Jain
Pranjal Jain

Ex-Microsoft SDE · IIT Kanpur · Founder of Prepflix. Helps engineers crack startup and product company interviews across India.