Scale Interview
System DesignMedium

Design Spotify

Music Streaming System Design

Overview

Open Spotify, hit play, and a song starts almost instantly — then the next track in your playlist begins the moment the current one ends, with no gap, even on a train with a flaky signal because you downloaded the album last night. Underneath sits a catalog of ~100M licensed tracks shared by half a billion listeners, personalized playlists that feel hand-picked, and a library of playlists you can build, reorder, and share.

Music streaming looks like video streaming, and it shares the same backbone — media in blob storage, served through a CDN, with the metadata in a database. If you have read the video streaming breakdown, you already own the CDN, blob-storage, and adaptive-bitrate fundamentals — so this walkthrough does not re-derive them. Instead it focuses on what makes audio different: files are tiny (a whole song is a few MB, not a gigabyte), the catalog is bounded and shared by everyone, playback must be gapless, and a huge fraction of listening happens offline from downloaded tracks. On top of delivery sit two product problems worth real design time — the read-heavy catalog + playlist model, and personalized recommendations (Discover Weekly).

In this walkthrough you'll scope the service, size it (the numbers look very different from video), model the catalog and playlists, design the API, draw the ingestion and playback paths, then go deep on audio delivery, the playlist data model, and the recommendation pipeline.

Functional requirements

  • Play a track. A listener hits play on a song and it starts quickly, streaming at a quality that suits their network and subscription tier.
  • Gapless, continuous playback. When one track ends, the next begins seamlessly — no buffering gap, no silence between tracks on an album or a DJ mix.
  • Browse the catalog. Look up tracks, albums, and artists; see an artist's discography and an album's track list.
  • Playlists and library. Create playlists, add/remove/reorder tracks, follow other users' playlists, and save songs to a personal library. Support collaborative playlists.
  • Offline download. Download tracks and playlists to a device and play them with no network connection (premium feature).
  • Personalized recommendations. Generate personalized playlists (Discover Weekly, Release Radar) and "recommended for you" surfaces.

Out of scope (say so): podcasts and audiobooks (similar delivery, different metadata), social features beyond playlist following, ads insertion and the free-tier ad server, payments/subscription billing, and the artist-facing analytics dashboard.

Tip

A strong opening: "This shares the CDN + blob-storage streaming backbone with video, so I'll lean on that and spend my time on what's genuinely different for music — tiny files with aggressive prefetch, gapless playback, offline sync, and the catalog/playlist and recommendation systems." Naming the reuse up front shows judgment and buys you time for the interesting parts.

Non-functional requirements

  • Instant start and gapless continuation. Time-to-first-note should feel instant, and the transition between tracks must have no audible gap. This is the headline experience and it differs from video, where a second of startup buffering is tolerated.
  • Very read-heavy, and heavily shared reads. A song is licensed once and streamed by millions. Unlike user-generated video, everyone streams the same bounded catalog, so caching is extraordinarily effective.
  • Offline-first on mobile. A large share of listening is offline or on poor networks; downloaded content must play with zero connectivity and sync when the network returns.
  • High availability over strong consistency. A newly added playlist track showing up a second late, or a stale follower count, is fine. Playback must never fail.
  • Global low latency. Listeners are everywhere; the first audio bytes should arrive from somewhere physically close.

Note

The two properties that most shape the audio design and separate it from video are shared, bounded catalog (near-perfect cacheability, small total storage) and offline playback (download + license + sync), which barely exists for a YouTube-style product. Anchor your answer on those.

Estimations

State assumptions out loud; the goal is to show how audio's numbers push the design away from video's, not to divide perfectly.

Rounded assumptions

  • Catalog: ~100M tracks, growing slowly (~100K new tracks/day, licensed — not an open upload firehose).
  • Track size: average track ~3.5 min. At ~160 kbps that's ~4 MB; store ~3 quality tiers (96/160/320 kbps) → ~15 MB of media per track.
  • Users: ~500M monthly active, ~200M daily active; a listener plays ~40 tracks/day.
  • Streams/day: 200M × 40 ≈ ~8B track plays/day → ~90K plays/sec average, peaking a few × higher.
  • Concurrent listeners at peak: on the order of ~50M streaming at once.
  • Average bitrate: ~160 kbps ≈ 0.16 Mbps per stream.

Storage

  • 100M tracks × ~15 MB = ~1.5 PB for the whole catalog. That is small — it fits comfortably and grows slowly. (Contrast: a UGC video platform adds ~1 PB per day.)

Egress

  • 50M concurrent × 0.16 Mbps ≈ ~8 Tbps at peak. Large, so a CDN is still mandatory — but per-stream it is ~30× lighter than 1080p video, and because everyone streams the same catalog, edge cache hit ratios approach 100% for the popular head.

How the numbers affect the design

Signal from the numbers Design decision
A whole track is only ~4 MB Prefetch the entire next track early; cache full songs on device — no need for per-segment ABR gymnastics.
Catalog is small (~1.5 PB) and bounded Storage is a non-issue; can even pre-push the popular head to every edge instead of pull-on-demand.
Everyone streams the same tracks Cache hit ratios near 100% for hot tracks — the CDN offloads almost all egress cheaply.
Reads ≫ writes, catalog changes slowly Optimize and cache the catalog read path hard; ingestion is a modest background pipeline.
Large share of listening is offline First-class download + DRM license + sync subsystem, not an afterthought.
Recommendations are a core product surface A separate batch recommender pipeline (candidate gen + ranking), not part of delivery.

Caution

Don't copy the video design wholesale and spend the interview on the transcoding pipeline and 25 Tbps egress. Audio media is small and cheap; the transcode is a light re-encode into a few bitrates. The interesting problems here are gapless + prefetch + offline, playlists at read scale, and recommendations — spend your time there.

Core entities / data model

As with video, keep small structured metadata in a database and large media bytes in blob storage, with the blob key stored in the database. The catalog is highly relational (tracks belong to albums, albums to artists), and playlists are a classic ordered-list-per-user problem.

Entity Key fields
Track track_id (PK), album_id, title, duration_ms, isrc, explicit, popularity, audio-feature vector ref
Album album_id (PK), artist_id, title, release_date, cover_art_url
Artist artist_id (PK), name, genres, follower_count
AudioAsset asset_id, track_id, bitrate/codec (e.g. 320k Ogg), blob_key, encoder-delay/padding samples
User user_id, display_name, country (licensing region), subscription_tier
Playlist playlist_id, owner_id, name, is_collaborative, follower_count, updated_at
PlaylistTrack playlist_id, track_id, position (or ordering key), added_by, added_at
LibrarySave user_id, track_id/album_id, saved_at — the "liked songs" / saved items

A few points worth saying out loud:

  • The catalog is read-mostly and highly cacheable. Track/album/artist rows change rarely and are shared by everyone, so they cache beautifully. A relational store fits the many-to-one relationships (track → album → artist); a large read replica fleet plus caching absorbs the read load.
  • Media never lives in the database. AudioAsset.blob_key points into blob storage; the encoded audio itself is served from the CDN. One track fans out into a few AudioAssets (one per bitrate).
  • PlaylistTrack is an ordered list, and ordering is the hard part. Naively storing an integer position makes an insert-in-the-middle or a reorder an O(n) renumber of every following row. Deep dive 2 covers ordering keys and collaborative edits.
  • Encoder delay/padding is metadata, not a nicety. Lossy encoders add silent priming samples at the start and padding at the end; gapless playback needs the exact sample counts to trim them (deep dive 1), so store them with the asset.

Warning

Don't store audio bytes in the metadata database as BLOB columns. Keep media in blob storage and store only the key — the same rule as video, for the same reason: databases are for many small structured rows, not media files.

API design

Four things matter: reading catalog metadata, getting a track's playable audio, managing playlists, and fetching recommendations. Catalog and playlist calls are ordinary REST; playback is a client contract where the player prefetches ahead.

Get track metadata + stream sources

GET /api/tracks/{id}
200 OK
Returns: {
  "track_id": "t_123", "title": "...", "duration_ms": 214000, "album": {...},
  "stream": {
    "320k": "https://cdn.../t_123/320.ogg?signed...",
    "160k": "https://cdn.../t_123/160.ogg?signed...",
    "encoder_delay": 576, "encoder_padding": 1152
  }
}

Return the facts about the track plus signed, expiring CDN URLs for each available bitrate (gated by the user's subscription tier and licensing region). Audio files are small enough that the player usually fetches one whole file per track using HTTP range requests, rather than a many-segment manifest.

Playback (client contract)

The player doesn't just fetch the current song — it looks ahead:

1. GET /api/tracks/{current}      → metadata + signed stream URLs
2. GET current audio from CDN     (range requests; start playing after a small buffer)
3. GET /api/tracks/{next}         → prefetch the NEXT track's metadata + URLs
4. GET next audio from CDN early  → buffer the head of the next track before the current one ends
5. at the boundary: trim padding/delay and play through with NO gap  (deep dive 1)

Manage playlists

POST  /api/playlists                         → create, returns playlist_id
PATCH /api/playlists/{id}/tracks             → add / remove / reorder
        Body: { "op": "move", "track_id": "...", "after": "<ordering_key>" }
GET   /api/playlists/{id}?limit=100&cursor=  → paginated ordered tracks
POST  /api/playlists/{id}/followers          → follow a playlist

Reorder is expressed relative to a neighbor (after: <ordering_key>) rather than an absolute index, so a single move is a cheap point update, not a renumber (deep dive 2).

Get recommendations

GET /api/me/discover-weekly    → the precomputed weekly personalized playlist
GET /api/me/recommendations?seed=track:t_123   → contextual "more like this"

Discover Weekly is precomputed offline and simply read here; contextual recs may do light read-time ranking over a precomputed candidate set (deep dive 3).

Tip

Keep the API boring: metadata in, signed CDN URLs out, playlist edits as small relative operations. All the difficulty lives in the prefetch/gapless client contract, the playlist ordering model, and the recommendation pipeline — not the API shape.

High-level architecture

Three loosely-coupled pieces: a modest ingestion path (labels deliver tracks → encode → CDN), a cache-everything playback + catalog path (read), and the playlist/library service. We'll walk each, then combine.

1. Ingestion path (write)

Record labels deliver masters; the platform encodes each into a few bitrates and stages them at the CDN. This is a slow, bursty background job — but far lighter than video transcoding, and it runs on a bounded ~100K tracks/day, not an open upload firehose.

flowchart LR
    Label["Label / distributor"] -->|"deliver master + metadata"| Ingest["Ingestion Service"]
    Ingest -->|"catalog rows"| MetaDB[("Catalog DB")]
    Ingest -->|"enqueue encode job"| Queue["Encode Job Queue"]
    Queue --> Workers["Encode Workers"]
    Workers -->|"96 / 160 / 320 kbps assets"| Blob[("Blob Storage")]
    Blob -.->|"pre-push popular head to edges"| CDN[("CDN")]
    Workers -->|"mark track AVAILABLE"| MetaDB

Each worker re-encodes the master into a few bitrates (a light CPU job — seconds, not the minutes video needs), stores them as blobs, records the encoder delay/padding per asset, and marks the track available. Because the catalog is small and shared, popular tracks can be pre-pushed to every edge rather than waiting for a first-listener miss.

2. Playback + catalog read path

Playback is read-heavy and served almost entirely from cache and CDN — your servers handle only the small metadata lookup and URL signing.

flowchart LR
    Player["Player"] -->|"1. GET /tracks/id"| Catalog["Catalog Service"]
    Catalog -->|"hit"| Cache[("Metadata Cache")]
    Cache -.->|"miss"| MetaDB[("Catalog DB")]
    Catalog -->|"2. signed CDN URLs"| Player
    Player -->|"3. GET audio (current + prefetch next)"| CDN[("CDN edge")]
    CDN -->|"3a. edge hit: serve bytes"| Player
    CDN -.->|"3b. edge miss: origin fetch once"| Blob[("Blob Storage")]
  1. The player asks the Catalog Service for track details; that tiny lookup is served from cache, falling back to the catalog DB on a miss.
  2. The response includes signed, expiring CDN URLs per bitrate (tier- and region-gated).
  3. The player streams the current track and prefetches the next from the nearest edge. Because the catalog is shared and bounded, hot tracks are almost always an edge hit — the origin is barely touched.

3. Playlist / library service + combined view

Playlist and library operations are their own read-heavy service over the ordered-list model, with recommendations produced by an offline pipeline that writes results back for the read path to serve.

flowchart LR
    Player["Player"] -->|"metadata"| Catalog["Catalog Service"]
    Catalog --> Cache[("Cache")]
    Cache -.-> MetaDB[("Catalog DB")]
    Player -->|"audio"| CDN[("CDN edge")]
    CDN -.-> Blob[("Blob Storage")]
    Player -->|"playlists / library"| PL["Playlist Service"]
    PL --> PLDB[("Playlist DB")]
    Player -->|"discover weekly"| RecStore[("Recommendation Store")]
    Events["Play events"] --> Stream[["Event Stream (Kafka)"]]
    Stream --> RecPipe["Recommendation Pipeline (batch)"]
    RecPipe --> RecStore

The three read services (catalog, playlist, recommendations) scale independently. Play events flow to a stream that feeds the recommendation pipeline (and analytics), which writes precomputed playlists back to a store the read path serves cheaply.

Tip

The through-line: a bounded, shared catalog makes the read path (catalog + audio) cache-dominated and cheap, which frees you to invest engineering in the two things that actually differentiate a music app — flawless playback (gapless + offline) and recommendations.

Deep dives

1. Audio streaming delivery — prefetch, gapless, and offline sync

The delivery backbone (blob storage + CDN + a few bitrates) is identical to video streaming, so reuse it and don't re-derive it. What's different is entirely a consequence of audio files being tiny and playback being continuous and often offline.

Whole-file prefetch instead of segment-by-segment ABR. A 1080p video is gigabytes, so it must be chopped into segments and adapted per segment. A song is ~4 MB — small enough to fetch as a single file with HTTP range requests, buffer quickly, and even hold entirely in memory. Adaptive bitrate still exists, but it's coarse: the client picks one quality tier at track start based on network and subscription (96k on a weak cell signal, 320k on wifi for a premium user), rather than switching every few seconds mid-song. The decisive move for music is prefetching the entire next track while the current one plays — cheap because the file is small, and the thing that makes the next-track transition instant.

Gapless playback. Albums and DJ mixes are mastered so tracks flow into each other with no silence. Two problems break this and both must be solved:

  • Buffering gap: if the player only starts fetching track N+1 when track N ends, the network round-trip creates an audible pause. The fix is the prefetch above — the head of the next track is already buffered before the current one finishes, and the audio pipeline is fed continuously across the boundary.
  • Encoder delay/padding: lossy codecs (MP3, AAC, Ogg Vorbis) add a few hundred priming samples of silence at the start of every file and pad the end to a whole frame. Play two encoded files back-to-back naively and you get a tiny silence at every join. The fix is to store the exact encoder delay and padding sample counts as metadata (from the LAME tag / iTunSMPB / Ogg granule positions) and have the player trim them, then feed the trimmed PCM of track N+1 immediately after track N's last real sample.
sequenceDiagram
    participant P as Player
    participant CDN as CDN edge
    Note over P: track N playing, ~20s left
    P->>CDN: prefetch head of track N+1
    CDN-->>P: audio bytes for N+1
    Note over P: decode N+1, trim its priming samples
    Note over P: track N reaches last real sample, trim N's trailing padding
    P->>P: feed N+1 PCM immediately, no gap

Warning

Don't wait until a track ends to fetch the next one, and don't ignore encoder delay/padding. Either mistake produces an audible gap — the exact thing gapless playback exists to prevent. Prefetch early and trim the priming/padding samples.

Offline download and sync. A large fraction of listening is offline, so downloads are a first-class subsystem, not a cache side effect:

  • Download = encrypted file + license. The client downloads the encoded track (typically DRM-encrypted) plus a license granting offline playback for a bounded period. The license is tied to the account/device and expires (e.g. ~30 days), so the app must check in online periodically to renew it — this enforces that lapsed subscribers can't keep playing downloads forever.
  • Sync, not just copy. Downloaded playlists must stay consistent with the server: if you add or remove a track online, the device reconciles on next connectivity — fetching newly added tracks and evicting removed ones. This is a delta sync keyed on playlist version, not a re-download.
  • Device-side limits and eviction. Downloads are capped (per account and per device storage), stored in an app-private encrypted cache, and evicted under an LRU/user-managed policy.
flowchart LR
    App["Mobile app"] -->|"1. request download"| DL["Download Service"]
    DL -->|"2. license (expiring) + encrypted asset URLs"| App
    App -->|"3. fetch encrypted audio"| CDN[("CDN edge")]
    App -->|"4. store in encrypted device cache"| Local[("On-device cache")]
    App -.->|"5. periodic online check-in: renew license, sync playlist deltas"| DL

Caution

Offline is where correctness bites. If the license never expires, subscription enforcement breaks; if playlist edits don't sync, the downloaded copy silently diverges from the server. Model the license lifetime and the delta-sync explicitly — reviewers probe here because it's the part that has no video analogue.

2. Catalog and playlists — a read-heavy, ordered-list model

Catalog reads are shared and cache first. Track/album/artist metadata is small, changes rarely, and is requested by everyone, so it is a textbook caching target. Serve it from an in-memory cache (cache-aside) backed by a relational store with read replicas. The relational model fits the hierarchy (track → album → artist, plus many-to-many for featured artists and genres), and denormalizing the hot read shapes — e.g. an album's track list, an artist's top tracks — into the cache keeps a page load to a couple of lookups.

Playlists are the interesting write. A playlist is an ordered list of tracks per user, and the hard part is cheap inserts, moves, and reorders while other collaborators edit concurrently. The naive model — an integer position column — is a trap:

Integer position column — renumber on every insert/move (avoid)

Store PlaylistTrack.position as 0,1,2,3… and keep them dense. Inserting at position 2 shifts every later row down by one.

  • Pro: dead simple; ordered read is a trivial ORDER BY position.
  • Con: an insert or move in the middle of a 500-track playlist rewrites hundreds of rows, and two collaborators editing at once race on the same positions — lost updates or duplicate positions. Every reorder is O(n) writes.
  • Verdict: fine for a tiny toy list, wrong for collaborative playlists at scale.
Fractional / lexicographic ordering keys — O(1) inserts and moves (recommended)

Give each item an ordering key (a fractional number, or better a lexicographic rank string as in the fractional-indexing / LexoRank approach) instead of a dense index. To insert between two items, pick a key strictly between their keys; to move an item, assign it a new key between its new neighbors. Read order is ORDER BY ordering_key.

  • Pro: an insert or move touches one row — no renumbering. The relative after: <key> API maps directly onto it. Concurrent edits at different spots don't collide.
  • Con: keys can grow long after many inserts at the same spot, needing an occasional background rebalance; concurrent inserts at the exact same position need a tiebreak (append the editor's id) to stay deterministic.
  • Verdict: the standard solution for ordered lists and collaborative editing. It's what makes reorder a cheap point update.

Collaborative editing. A collaborative playlist takes concurrent edits from several users. Because ordering keys make each edit a local, independent operation, most concurrent edits commute — two people adding songs at different spots don't conflict. Treat edits as an append-only log of operations (add/remove/move) applied in timestamp order; last-write-wins on the same item is acceptable for this product (a playlist is not a bank ledger). Clients get near-real-time updates via a lightweight push/poll, and offline edits reconcile on reconnect.

Read scale and following. Popular editorial playlists (e.g. "Today's Top Hits") are followed by tens of millions and read constantly — cache them hard, and treat the follower relationship like a social graph edge. Follower counts and "playlists containing this track" are eventually consistent, aggregated off the hot path.

Note

The playlist ordering key is the one genuinely non-obvious data-modeling decision in this whole design. If you reach for a dense integer position and then hand-wave the reorder cost, an interviewer will push on it. Naming fractional/lexicographic ranking (and why it makes moves O(1)) is a strong signal.

3. Music recommendations — Discover Weekly at scale

Personalized playlists like Discover Weekly are a core product surface, and they're a recommendation system in the same family as the short-video recommender — so reuse that machinery (candidate generation → ranking, embeddings + ANN, offline training loop) rather than reinventing it. The music-specific differences are what to emphasize.

Batch, not real-time. Unlike a swipe-by-swipe video feed that must rank in <200 ms, Discover Weekly is generated once a week per user, offline, and simply read at request time. This flips the constraint: throughput and quality matter, tail latency doesn't. The pipeline runs as a big batch job, writes a ready-made playlist per user to a store, and the API just serves it. Contextual "more like this" (radio, autoplay after a song ends) does need low-latency read-time ranking, but over a small precomputed candidate set.

Two complementary signals — collaborative filtering + audio content.

Signal What it captures Why music needs it
Collaborative filtering "People with your taste also love X" — learned from the user×track play matrix The dominant signal; captures taste and co-listening better than any content feature
Audio / content features Tempo, energy, acousticness, genre, plus embeddings from the raw audio and editorial/text metadata Cold start — a brand-new release has no plays yet, so content features let it be recommended before it has listening history

Blending the two is the standard approach: collaborative filtering carries the head, and content features cover the cold-start long tail (new releases, obscure tracks) that pure CF can't reach.

The pipeline mirrors the two-stage funnel from the video recommender:

flowchart LR
    Plays["Play / skip / save events"] --> Stream[["Event Stream"]]
    Stream --> Train["Batch training (weekly)"]
    Train -->|"user + track embeddings"| Emb[("Embedding store / ANN index")]
    Emb --> CandGen["Candidate generation (per user)"]
    Audio["Audio + editorial features"] --> Emb
    CandGen -->|"~hundreds of tracks"| Rank["Ranking + filtering"]
    Rank -->|"final 30-track playlist"| RecStore[("Recommendation Store")]
    RecStore --> API["GET /me/discover-weekly"]
  1. Candidate generation. From the user's taste embedding, retrieve a few hundred candidate tracks via ANN over track embeddings, blended with candidates from similar users' playlists and fresh releases in genres the user likes.
  2. Ranking + filtering. Score candidates for predicted engagement, then apply music-specific rules: filter out tracks the user already knows (Discover Weekly is for discovery — recently played/saved tracks are removed), cap tracks per artist for variety, and respect explicit/region constraints.
  3. Serve. Write the final ~30-track playlist to the recommendation store; the API reads it directly all week.
Pure collaborative filtering only — strong for the head, blind to new content

Recommend using only the user×track interaction matrix (matrix factorization or a two-tower model on plays).

  • Pro: captures taste and co-listening extremely well for tracks with enough plays; simple, proven, the backbone of most music recs.
  • Con: cold start — a track released this week has no interaction data, so pure CF can never surface it, exactly when Release Radar most needs to. Also biases toward already-popular tracks.
  • Verdict: necessary but not sufficient. Blend with content/audio features so new and long-tail tracks are reachable.
Hybrid: collaborative filtering + audio/content embeddings (recommended)

Use CF for the head and content-based embeddings (audio features + metadata) to place new and obscure tracks into the same embedding space, so they're retrievable before they have plays.

  • Pro: covers both established taste and cold-start discovery; new releases can be recommended on day one from their audio/editorial features.
  • Con: two models to train and blend; content embeddings are a rough proxy for taste until real plays arrive to correct them.
  • Verdict: the standard production design. Precompute weekly, serve the finished playlist cheaply.

Tip

Say the batch/offline part explicitly: "Discover Weekly is generated per user offline once a week and just read at request time, so I optimize for quality and throughput, not latency." Then note the two music-specific twists — content features for cold start and filtering out already-known tracks — which distinguish a music recommender from a generic feed.

Tradeoffs & bottlenecks

  • Shared catalog makes caching a superpower — lean into it. Because everyone streams the same bounded ~100M tracks, edge hit ratios for the popular head approach 100% and you can even pre-push it. The cost is negligible storage; the win is that egress, the scary number for video, is largely a solved problem here.
  • Prefetch depth: instant transitions vs wasted bandwidth. Prefetching the next track makes gapless transitions instant but wastes bandwidth/data if the user skips before reaching it. Prefetch one track ahead (not five), and on cellular respect data-saver settings.
  • Bitrate tiers vs storage/quality. More tiers (96/160/320, plus lossless) fit more networks and please audiophiles but multiply stored assets. Storage is cheap here, so the real constraint is licensing and product tiering, not bytes.
  • Playlist ordering: simple vs cheap edits. A dense integer position is simplest to read but O(n) to edit; fractional/lexicographic keys make edits O(1) at the cost of occasional key rebalancing. At collaborative-playlist scale the ordering key wins.
  • Offline license lifetime: convenience vs enforcement. A longer offline license means fewer forced check-ins (better for users with poor connectivity) but weaker subscription enforcement. ~30 days with periodic renewal is the usual balance.
  • Recommendation freshness: batch vs real-time. Weekly batch generation is cheap and high-quality but can't react within a session; contextual radio/autoplay adds a light real-time ranking layer over precomputed candidates to cover in-session reactivity.
  • Consistency is intentionally relaxed. A playlist edit, follower count, or new recommendation being slightly stale is fine — and that relaxation is what lets the whole read side cache aggressively.

Extensions if asked

Add only the extension that changes the design discussion:

  • Free tier with ads. Insert audio ads between tracks for non-premium users, with frequency capping and targeting — a separate ad-decisioning service invoked in the playback contract, plus a lower bitrate cap and no offline downloads for free users.
  • Lossless / hi-fi audio. Add a FLAC/lossless tier (~1000+ kbps). Files jump to tens of MB, which stresses prefetch and offline storage budgets and reopens the ABR discussion for constrained networks.
  • Real-time collaborative session ("Jam" / group listening). Multiple users listen in sync to the same playback position — a real-time presence + playback-state sync problem (closer to the live/real-time fan-out family) layered on top of delivery.
  • Podcasts and audiobooks. Same delivery backbone, but long-form (hours, chapters, resume-position sync across devices) and different discovery/metadata; audiobooks add per-title purchase/entitlement.
  • Play-count royalties and analytics. Emit a play event per stream to a pipeline that aggregates counts for royalty accounting and the artist dashboard — never by incrementing a counter on the hot read path.

What interviewers look for & common mistakes

What interviewers usually reward:

  • Reusing the streaming backbone deliberately — recognizing music shares CDN + blob storage + a few bitrates with video, saying so, and not burning the interview re-deriving transcoding and egress.
  • Deriving audio's distinct numbers — tiny files, small bounded catalog, shared reads with near-100% cache hits — and letting them drive the design (whole-file prefetch, pre-push, offline).
  • Gapless playback — explaining both the prefetch (no buffering gap) and the encoder delay/padding trimming, not just hand-waving "play the next song."
  • Offline as a first-class subsystem — download + expiring DRM license + periodic check-in + playlist delta sync — the part with no video analogue.
  • The playlist ordering model — choosing fractional/lexicographic keys over integer positions so inserts, moves, and collaborative edits are cheap.
  • Recommendations as an offline batch pipeline — candidate gen + ranking, CF blended with content features for cold start, filtering out already-known tracks; reusing the recommender machinery rather than reinventing it.

Before you finish, do a quick mistake check:

  • Did you cross-link the video design and reuse the CDN/blob/ABR fundamentals instead of re-deriving them?
  • Did you use audio's numbers (small files, bounded shared catalog) to justify whole-file prefetch and near-perfect caching?
  • Did you explain gapless playback as both prefetch and encoder-delay/padding trimming?
  • Did you make offline a real subsystem with an expiring license and playlist sync, not just "cache the file"?
  • Did you avoid the integer-position trap and use an ordering key for playlists?
  • Did you design recommendations as a precomputed batch pipeline that blends collaborative filtering with content features for cold start?

Practice this live

Run this exact question with our AI voice interviewer and get feedback.

Start the interview →