Spotify System Design — 1 Hour Interview Guide
Design a scalable music streaming platform like Spotify. Covers requirements, capacity estimation, audio streaming pipeline, OGG Vorbis encoding, CDN strategy, search, Discover Weekly recommendation engine, database design, caching, offline DRM, and real-time Spotify Connect — all in a 1-hour interview format.
1-Hour Interview Roadmap
| Time | Topic |
|---|---|
| 0 – 5 min | Requirements clarification |
| 5 – 10 min | Capacity estimation |
| 10 – 18 min | High-level architecture + core components |
| 18 – 28 min | Audio upload pipeline + encoding |
| 28 – 36 min | Audio streaming + CDN strategy |
| 36 – 46 min | Search + Discover Weekly recommendation engine |
| 46 – 54 min | Database design + caching |
| 54 – 58 min | Scaling + offline playback + social features |
| 58 – 60 min | Trade-offs + failure scenarios |
What Are We Building?
A music and podcast streaming platform where users can:
- Stream millions of songs and podcasts on demand
- Search by artist, album, song, genre, or mood
- Create, share, and follow playlists
- Get personalized music recommendations (Discover Weekly, Daily Mixes)
- Download tracks for offline playback
- Follow artists and friends, see what they're listening to
- See real-time "Now Playing" across all connected devices
Scale reference: Spotify has 602 million monthly active users, 240 million paid subscribers, 100 million+ tracks in catalog, and streams audio to 80 million concurrent listeners at peak.
Key difference from YouTube/Netflix: Spotify streams audio only — files are 3–10 MB vs 2 GB for video. This makes CDN caching far more aggressive. The real hard challenge is the recommendation engine — Spotify's Discover Weekly is widely considered the best music recommendation system ever built.
Step 1 — Requirements
Functional Requirements
| # | Requirement |
|---|---|
| 1 | Users can search for songs, albums, artists, playlists, and podcasts |
| 2 | Users can stream any track on demand with < 250ms playback start time |
| 3 | Users can create, edit, and share playlists |
| 4 | Users can follow artists, friends, and public playlists |
| 5 | Free users hear ads between tracks; Premium users stream ad-free |
| 6 | Premium users can download tracks for offline listening |
| 7 | Users receive personalized recommendations — Discover Weekly, Daily Mixes |
| 8 | Users see real-time "Now Playing" on their profile |
| 9 | Artists can upload tracks and view play count and listener analytics |
| 10 | Multiple devices: phone, desktop, web, TV, car, speaker — seamless handoff |
Non-Functional Requirements
| # | Requirement |
|---|---|
| 1 | Audio playback starts within 250ms — fast buffering, no startup lag |
| 2 | No buffering interruptions on 1 Mbps+ connections |
| 3 | High availability — 99.99% uptime |
| 4 | Search results returned within 200ms |
| 5 | Recommendations refreshed weekly (Discover Weekly) and daily (Daily Mixes) |
| 6 | Offline content must remain playable up to 30 days after download |
| 7 | Eventual consistency acceptable for play counts, follower counts, playlists |
| 8 | Strong consistency required for subscription status and payment |
Out of Scope
- Live audio and concert streaming
- Payment and billing infrastructure
- Podcast recording tools
- Artist royalty calculation engine
- Advertising platform
Step 2 — Capacity Estimation
Assumptions
| Metric | Value |
|---|---|
| Monthly Active Users (MAU) | 600 million |
| Daily Active Users (DAU) | 250 million |
| Peak concurrent listeners | 80 million |
| Total tracks in catalog | 100 million |
| Average track size (128 Kbps MP3) | 3.5 MB |
| Average track size (320 Kbps MP3) | 8.5 MB (Premium) |
| Average track size (OGG Vorbis HQ) | 6 MB |
| Average listen time per user/day | 30 minutes |
| Tracks played per user/day | ~6 songs |
| New tracks uploaded per day | ~60,000 |
Storage Estimation
Catalog: 100M tracks × 3 formats × 6 MB avg = ~1.8 PB total audio storage
New daily: 60K tracks × 3 formats × 6 MB = ~1 TB/day
Podcast episodes: ~5M episodes × 50 MB avg = ~250 TB
Streaming Bandwidth at Peak
80M concurrent streams × 128 Kbps (free) or 320 Kbps (premium)
Average: ~200 Kbps across user base
80M × 200 Kbps = 16 Tbps peak outbound bandwidth
CDN absorbs ~90% → origin bandwidth ≈ 1.6 Tbps
Search and API Traffic
250M DAU × 5 searches/day = 1.25 billion searches/day
= ~14,500 searches/second (sustained)
= ~50,000 searches/second (peak)
Key Insight
Spotify is fundamentally different from Netflix and YouTube. Audio files are small and immutable (3–10 MB vs gigabytes for video). The entire catalog (~1.8 PB) can potentially be cached at CDN edge nodes. The harder challenge is not delivery — it is the recommendation engine that generates 40+ personalized playlists per user per week.
Step 3 — High-Level Architecture
flowchart TD
Client[Client\nMobile / Desktop / Web] --> AG[API Gateway\nAuth + Rate Limiting]
AG --> SS[Stream Service]
AG --> SRCH[Search Service]
AG --> REC[Recommendation Service]
AG --> PLS[Playlist Service]
AG --> USER[User Service]
AG --> ART[Artist Service]
SS --> CDN[CDN\nAudio Segments]
CDN -->|cache miss| AS[Audio Storage\nS3]
ART --> KF[Kafka\ntrack.uploaded]
KF --> ENC[Encoder Service\nFFmpeg]
ENC --> AS
KF --> IDX[Search Indexer\nElasticsearch]
KF --> RC[Recommendation\nPipeline\nSpark + TF]
USER --> UDB[(PostgreSQL\nUser Profiles)]
PLS --> PDB[(Cassandra\nPlaylists)]
REC --> RDB[(Redis\nRec Cache)]
SRCH --> ES[(Elasticsearch\nMusic Index)]
AG --> NP[Now Playing\nService]
NP --> WS[WebSocket\nReal-time]
NP --> NPDB[(Redis\nNow Playing State)]
Component Responsibilities
| Component | Responsibility |
|---|---|
| API Gateway | JWT auth, plan check (Free vs Premium), rate limiting, routing |
| Stream Service | Generate pre-signed CDN URLs for audio segments; track play events |
| Encoder Service | Transcode uploaded audio into multiple quality tiers; generate waveform data |
| Search Service | Full-text search over tracks, artists, albums, playlists via Elasticsearch |
| Recommendation Svc | Serve pre-computed personalized playlists; Discover Weekly, Daily Mix |
| Playlist Service | CRUD for user playlists; collaborative playlist support |
| User Service | Profiles, follows, subscription status, preferences |
| Artist Service | Track upload, analytics, album management |
| Now Playing Service | Real-time "what is this user listening to" — WebSocket state sync |
| CDN | Global edge delivery of audio files — cache most of the catalog |
Step 4 — Audio Upload and Encoding Pipeline
Upload Flow (Artist Side)
sequenceDiagram
participant Artist
participant ART as Artist Service
participant MDB as Metadata DB (PostgreSQL)
participant S3 as Raw S3
participant KF as Kafka
participant ENC as Encoder Service
participant S3A as Processed S3
Artist->>ART: Upload track {title, artist, album, genre, tags}
ART->>MDB: INSERT track {status: PROCESSING}
MDB-->>ART: track_id
ART->>S3: Generate pre-signed URL
ART-->>Artist: {track_id, upload_url}
Artist->>S3: PUT raw audio file (WAV/FLAC/MP3)
S3-->>Artist: Upload complete
Artist->>ART: POST /tracks/{track_id}/complete
ART->>KF: Publish track.uploaded {track_id, raw_s3_key}
KF->>ENC: Consume event
ENC->>S3: Download raw file
ENC->>ENC: Encode to 4 quality tiers
ENC->>S3A: Store encoded files
ENC->>MDB: UPDATE status = LIVE, set streaming_urls
ENC->>KF: Publish track.ready {track_id}
Audio Encoding — Quality Tiers
flowchart LR
RAW[Raw Audio\nWAV / FLAC] --> ENC[Encoder\nFFmpeg]
ENC --> T1[OGG Vorbis\n96 Kbps\nFree / Low Data]
ENC --> T2[OGG Vorbis\n160 Kbps\nFree / Normal]
ENC --> T3[OGG Vorbis\n320 Kbps\nPremium]
ENC --> T4[AAC\n256 Kbps\nApple devices]
ENC --> WV[Waveform JSON\nfor scrubber visualization]
T1 & T2 & T3 & T4 --> S3[Processed S3]
S3 --> CDN[CDN Edge]
| Quality Tier | Format | Bitrate | Users Served | File Size (3 min) |
|---|---|---|---|---|
| Low | OGG Vorbis | 96 Kbps | Free on low data | ~2 MB |
| Normal | OGG Vorbis | 160 Kbps | Free (default) | ~3.5 MB |
| High | OGG Vorbis | 320 Kbps | Premium | ~7.2 MB |
| Very High (HiFi) | AAC / FLAC | 256 Kbps+ | Premium / Spotify HiFi | ~6 MB |
Why OGG Vorbis (Not MP3)?
Spotify chose OGG Vorbis as its primary codec because:
- Better quality-per-bit than MP3 at the same bitrate
- Royalty-free — no licensing fees per stream (unlike MP3)
- Better suited for streaming — gapless playback, no encoder delay
Step 5 — Audio Streaming Flow
What Happens When You Press Play
sequenceDiagram
participant Client
participant AG as API Gateway
participant SS as Stream Service
participant CDN
Client->>AG: GET /tracks/{track_id}/stream
AG->>AG: Check JWT + subscription plan
AG->>SS: Forward request
SS->>SS: Determine quality tier\n(Free=160Kbps, Premium=320Kbps)
SS->>SS: Generate pre-signed CDN URL\n(expires in 60 seconds)
SS-->>Client: {stream_url: "cdn.spotify.com/t/abc.ogg?sig=...", expires_at: ...}
Client->>CDN: GET cdn.spotify.com/t/abc.ogg
CDN-->>Client: Stream audio bytes (range requests)
Client->>AG: POST /events/play {track_id, position_ms, context}
Note over Client,AG: Fire-and-forget play event → Kafka → analytics
Chunked Streaming with HTTP Range Requests
Spotify does not download the full audio file before playback. It uses HTTP range requests to stream in chunks:
Client requests:
GET /audio/track_abc.ogg HTTP/1.1
Range: bytes=0-131071 ← first 128 KB
Server responds:
HTTP/1.1 206 Partial Content
Content-Range: bytes 0-131071/7340032
[128 KB of audio data]
Player buffers 30 seconds ahead, then requests next chunk:
Range: bytes=131072-262143 ← next 128 KB
Benefits:
- Playback starts after buffering just the first chunk (~1 second of audio)
- Seeking jumps directly to the byte offset for the target timestamp
- No wasted bandwidth if the user skips a track after 5 seconds
CDN Strategy for Audio
Audio files are small and immutable — a track's bytes never change after encoding. This makes them ideal for aggressive CDN caching:
flowchart LR
Client -->|GET track_abc_320.ogg| CDN_EDGE[CDN Edge\nNearest PoP]
CDN_EDGE -->|Cache HIT ~90%| Client
CDN_EDGE -->|Cache MISS| S3[Processed S3]
S3 --> CDN_EDGE
CDN_EDGE --> Client
| Content | CDN TTL | Reasoning |
|---|---|---|
| Audio files | 1 year | Immutable — bytes never change after encoding |
| Album artwork | 30 days | Rarely updated |
| Waveform JSON | 7 days | Computed once; occasionally updated |
| Track metadata | 1 hour | Play counts change; title/artist rarely change |
Top 10% of Spotify's catalog = ~90% of all streams (Pareto distribution). Top 10M tracks × 6 MB avg = ~60 TB — fully cacheable at major CDN PoPs.
Step 6 — Search
Search Requirements
- Search by song title, artist name, album name, genre, mood, playlist name
- Autocomplete while typing
- Results ranked by relevance and popularity
- Filter by content type (track, album, artist, playlist, podcast)
- Results returned within 200ms
Search Architecture
flowchart LR
KF[Kafka\ntrack.ready\nartist.updated] --> IDX[Search Indexer]
IDX --> ES[Elasticsearch\nMusic Index]
Client -->|"Bohemian Rhapsody"| SRCH[Search Service]
SRCH -->|multi-match query| ES
ES -->|Top 50 results| SRCH
SRCH -->|Enrich + personalize rank| RDB[(Redis\nUser Context)]
SRCH --> Client
Elasticsearch Track Document
{
"track_id": "4u7EnebtmKWzUH433cf5Qv",
"title": "Bohemian Rhapsody",
"title_suggest": "Bohemian Rhapsody",
"artist": "Queen",
"album": "A Night at the Opera",
"genres": ["rock", "classic rock", "arena rock"],
"mood_tags": ["epic", "emotional", "dramatic"],
"release_year": 1975,
"duration_ms": 354000,
"play_count": 2800000000,
"popularity": 98,
"language": "en",
"is_explicit": false
}
Search Ranking Formula
score = BM25(query_text, title, artist, album)
× log(play_count + 1) ← global popularity boost
× genre_affinity(user, track) ← personalized boost
× recency_factor ← new releases boosted
Personalized ranking matters: a metal fan searching "queen" gets Queen the band; a chess fan needs disambiguation. Spotify knows your genre preferences from listening history.
Step 7 — Recommendation Engine (Discover Weekly)
Why Recommendations Are Spotify's Core Differentiator
Spotify's Discover Weekly playlist — 30 songs every Monday, personalized per user — has been described as the best recommendation system ever built. 40 million users listen to it every week. It drives more engagement than any other feature.
Three Types of Signals Spotify Uses
| Signal Type | What It Is | Example |
|---|---|---|
| Collaborative filtering | Users who listened to what you listened to also liked... | You like Radiohead → recommend Thom Yorke solo |
| Content-based filtering | Audio feature analysis of tracks you love | Tempo, key, valence, energy, danceability |
| NLP on playlist names | Mine playlist titles and descriptions for mood context | "sad rainy day" playlist → detect mood context |
Audio Feature Analysis
Spotify's Echo Nest (acquired 2014) analyzes raw audio and extracts numerical features for every track:
{
"tempo": 120.0, ← BPM
"energy": 0.85, ← intensity (0.0–1.0)
"valence": 0.32, ← musical positivity (0.0–1.0)
"danceability": 0.71, ← rhythm strength
"acousticness": 0.12, ← acoustic vs electronic
"instrumentalness": 0.0, ← vocals present?
"loudness": -5.2, ← dB
"key": 5, ← musical key (C=0, C#=1...)
"mode": 1 ← major (1) or minor (0)
}
Tracks with similar feature vectors are "sonically similar" — this enables mood-based recommendations even for brand new tracks with no play history.
Recommendation Pipeline Architecture
flowchart TD
PH[Play History\nLikes, Skips\nPlaylist additions] --> KF[Kafka\nListen Events]
KF --> BATCH[Offline Batch\nApache Spark\nweekly training]
KF --> STREAM[Online Stream\nReal-time signals]
BATCH -->|User-track\nembedding vectors| FAISS[(FAISS\nVector Store)]
BATCH -->|Collaborative filter\nmatrix factorization| CF[(CF Model Store)]
STREAM -->|Recent context| RC[(Redis\nContext Cache)]
User -->|Monday morning| RS[Recommendation Service]
RS -->|User vector lookup| FAISS
FAISS -->|500 candidate tracks| RANK[Ranking Model\nNeural Net]
RANK -->|Context| RC
RANK -->|Top 30 ranked tracks| DW[Discover Weekly\nPlaylist]
DW --> PDB[(Cassandra\nPlaylist Store)]
DW --> RC2[(Redis\nPlaylist Cache)]
Discover Weekly Generation — Weekly Batch
Every Sunday night (low traffic):
1. For each of 600M users:
a. Build user embedding from last 2 weeks of listens
b. Find 500 nearest tracks using FAISS ANN search
c. Filter out: already heard, disliked, too similar to recent listens
d. Re-rank 500 → top 30 using neural net (energy curve, variety, context)
2. Write 600M × 30 = 18 billion (user_id, track_id) pairs to Cassandra
3. Cache each user's playlist in Redis (7-day TTL)
4. Monday 00:00 UTC → playlists go live for all users
Daily Mix Generation — Daily Batch
Daily Mixes are genre and mood clusters of the user's own taste:
User has listened to:
- 200 indie rock songs → Daily Mix 1: Indie Rock
- 150 jazz songs → Daily Mix 2: Jazz
- 80 hip-hop songs → Daily Mix 3: Hip-Hop
Each mix: blend of saved songs user loves + ~30% new discoveries
Updated daily with fresh discoveries
Step 8 — Database Design
Database Selection
| Data | Database | Reason |
|---|---|---|
| User profiles + accounts | PostgreSQL | ACID — subscription status must be strongly consistent |
| Track + album metadata | PostgreSQL | Relational — complex queries (artist → albums → tracks) |
| Playlists + tracks | Cassandra | Wide rows (playlist_id, position, track_id); very high volume |
| User listening history | Cassandra | Per-user time-series; append-only; very high write volume |
| Follows (user→artist) | Cassandra | Wide rows (user_id, artist_id); billions of edges |
| Search index | Elasticsearch | Full-text + audio features + NRT indexing |
| Recommendations cache | Redis | Pre-computed playlists; sub-ms reads |
| Now Playing state | Redis | Real-time per-user state; short TTL |
| Play counts | Redis → PostgreSQL | Counter — async flush to avoid hotspot write contention |
| Audio files | S3 + CDN | Object storage with global edge delivery |
Track Metadata Table (PostgreSQL)
Table: tracks
track_id VARCHAR(22) PRIMARY KEY (Spotify base62 ID)
title VARCHAR(255) NOT NULL
artist_id BIGINT FK → artists
album_id BIGINT FK → albums
duration_ms INT
release_date DATE
genres TEXT[]
mood_tags TEXT[]
explicit BOOLEAN DEFAULT false
play_count BIGINT DEFAULT 0 (eventually consistent)
popularity INT (0–100, computed weekly)
audio_features JSONB {tempo, energy, valence, danceability, ...}
streaming_urls JSONB {"96k": "...", "160k": "...", "320k": "..."}
waveform_url TEXT
status VARCHAR (PROCESSING | LIVE | TAKEDOWN)
uploaded_at TIMESTAMP
Playlist Table (Cassandra)
Table: playlists
Partition key: playlist_id VARCHAR
Clustering key: position INT (track order within playlist)
Columns:
track_id VARCHAR
added_by BIGINT (user_id who added this track)
added_at TIMESTAMP
PRIMARY KEY (playlist_id, position)
Query: SELECT * FROM playlists WHERE playlist_id = ? ORDER BY position ASC
For collaborative playlists, added_by tracks who added each track.
User Listening History (Cassandra)
Table: listening_history
Partition key: user_id BIGINT
Clustering key: played_at TIMESTAMP DESC
Columns:
track_id VARCHAR
ms_played INT (how long listened before skip)
context_type VARCHAR (playlist | album | artist | search | radio)
context_id VARCHAR (id of the playlist/album that was playing)
device_type VARCHAR (mobile | desktop | web | speaker)
PRIMARY KEY (user_id, played_at)
Used by the recommendation pipeline — what did you play, for how long, and in what context.
Follow Graph (Cassandra)
Table: user_follows_artist
user_id BIGINT
artist_id BIGINT
followed_at TIMESTAMP
PRIMARY KEY (user_id, artist_id)
Table: artist_followers
artist_id BIGINT
user_id BIGINT
followed_at TIMESTAMP
PRIMARY KEY (artist_id, user_id)
Two denormalized tables — one for "who does this user follow" (feed assembly), one for "who follows this artist" (fan-out on new release).
Step 9 — Caching Strategy
Redis Cache Map
| Cache | Key Pattern | TTL | Contents |
|---|---|---|---|
| Track metadata | track:{track_id} |
1 hr | Full track object (title, artist, urls) |
| Discover Weekly | dw:{user_id} |
7 days | 30 track IDs for this user's Discover Weekly |
| Daily Mix | dm:{user_id}:{mix_num} |
24 hrs | Track IDs for each Daily Mix |
| Home screen rows | home:{user_id} |
30 min | Pre-assembled home screen sections |
| Now Playing | nowplaying:{user_id} |
5 min | Current track + position + device |
| Play count | plays:{track_id} |
Eventual | Counter — async flush to PostgreSQL |
| Search autocomplete | suggest:{prefix} |
1 hr | Top 10 completions for typed prefix |
| Artist top tracks | top:{artist_id} |
1 hr | Top 10 tracks by play count |
| User followed artists | following:{user_id} |
30 min | List of followed artist IDs |
| Session + auth | session:{user_id}:{device} |
30 days | JWT + refresh token |
Play Count at Scale
Spotify tracks play counts for royalty calculations. At 80 million concurrent streams:
80M concurrent × average song length 3.5 min
= ~22M play events per minute
= ~370,000 play events per second
Incrementing a PostgreSQL counter 370K times/second would cause catastrophic lock contention.
Solution — Redis counter + async flush:
flowchart LR
PlayEvent -->|INCR plays:{track_id}| RC[(Redis)]
RC -->|every 30s| FLUSH[Batch Flush Worker]
FLUSH -->|UPDATE play_count += delta| PG[(PostgreSQL)]
FLUSH -->|async event| KF[Kafka → Royalty Pipeline]
Royalty calculations use the Kafka event stream — not the PostgreSQL counter directly.
Step 10 — Social and Real-Time Features
Now Playing
When a user plays a track, their profile shows "Currently playing: [song] by [artist]."
flowchart LR
Client -->|track started| NP[Now Playing Service]
NP -->|SETEX nowplaying:user_id 300| RC[(Redis\n5 min TTL)]
FriendClient -->|GET /user/{id}/nowplaying| NP
NP -->|GET nowplaying:{id}| RC
RC --> FriendClient
- Redis key expires after 5 minutes if no update (track ended or app closed)
- Friends see "Listening to X" in real time via WebSocket or polling
Multi-Device Handoff (Spotify Connect)
Spotify Connect lets you start listening on your phone and hand off to a speaker or desktop seamlessly:
flowchart LR
Phone -->|Start playing track X at position 1:23| NP[Now Playing Service]
NP -->|SETEX device_state:{user_id}| RC[(Redis)]
Speaker -->|WebSocket: new device joining| NP
NP -->|GET device_state:{user_id}| RC
RC -->|{track_id, position_ms: 83000}| NP
NP -->|Play track X from 1:23| Speaker
State stored in Redis per user_id:
{
"track_id": "4u7EnebtmKWzUH",
"position_ms": 83000,
"is_playing": true,
"device": "phone",
"volume": 80
}
Any device joining the session reads this state and resumes from the exact position.
Collaborative Playlists
Multiple users can add tracks to the same playlist simultaneously:
- Playlist stored in Cassandra — append-only adds are idempotent
- Conflicts (two users adding at position 5 simultaneously) resolved by timestamp ordering
- Real-time update pushed via WebSocket to all subscribers of that playlist
Step 11 — Offline Playback
How Offline Download Works
Premium users can download up to 10,000 tracks across 5 devices for offline listening.
flowchart LR
Client -->|Download track| AG[API Gateway\nCheck Premium status]
AG -->|Authorized| SS[Stream Service]
SS -->|Encrypted download URL| Client
Client -->|GET audio file + DRM key| CDN[CDN]
CDN --> Client
Client -->|Store encrypted locally| LocalDB[Encrypted local DB\nSQLite + AES-256]
DRM for Offline Files
Downloaded files are encrypted with a device-specific key:
- The file cannot be played on another device — bound to
device_id - License key expires after 30 days — user must reconnect to re-validate subscription
- If subscription lapses, keys are not renewed and offline files become unplayable
Storage on Device
Device local storage:
tracks/{track_id}/audio.ogg.enc ← AES-256 encrypted audio
tracks/{track_id}/metadata.json ← title, artist, artwork URL
license/{device_id}.key ← device-bound decryption key
playlists/{playlist_id}.json ← offline playlist manifest
Step 12 — Scaling
Stream Scaling
flowchart TD
80M_Listeners[80M Concurrent Listeners] --> CDN[CDN\n200+ Global PoPs]
CDN -->|~90% cache hit| Listener
CDN -->|10% cache miss| S3[Processed S3]
S3 --> CDN
- CDN absorbs 90% of traffic — S3 sees only 8M streams of origin traffic
- Add CDN capacity (new PoPs) to scale — no backend changes required
- Audio files are immutable — 1-year CDN TTL maximizes hit rate
API and Recommendation Scaling
flowchart LR
LB[Load Balancer] --> SS1[Stream Service Pod]
LB --> SS2[Stream Service Pod]
LB --> SS3[Stream Service Pod]
SS1 & SS2 & SS3 --> RC[(Redis Cluster\nCache)]
RC -->|miss| PG[(PostgreSQL\nRead Replicas)]
- All services are stateless — scale horizontally by adding pods
- Redis Cluster shards cache keys across nodes
- PostgreSQL read replicas absorb catalog reads
Recommendation Job Scaling
Weekly Discover Weekly batch:
600M users × 30 tracks = 18 billion playlist entries
Apache Spark on AWS EMR:
- Partition users into 10,000 batches of 60,000 users each
- Each batch processed by one Spark executor
- Full pipeline completes in ~4 hours (Sunday night)
- Monday 00:00 UTC: playlists written to Cassandra + Redis
Database Sharding
| Database | Shard Key | Notes |
|---|---|---|
| Tracks | artist_id |
All tracks by one artist co-located |
| Playlists | playlist_id |
Each playlist on one shard |
| History | user_id |
All listening history for one user on one shard |
| Follows | user_id |
All follow edges for one user on one shard |
Step 13 — Failure Scenarios
| Failure | Impact | Mitigation |
|---|---|---|
| CDN PoP outage | Listeners in that region experience buffering | CDN auto-routes to next nearest PoP; audio files also in S3 |
| Redis (recs cache) down | Home screen slow; Discover Weekly missing | Redis Sentinel failover; fallback to Cassandra playlist store |
| Recommendation service down | Home screen shows generic popular tracks | Serve cached last-known recommendations; "Top Tracks" fallback |
| Elasticsearch down | Search returns 503 | Graceful degradation — show trending / recent searches |
| Kafka down | Play events not recorded; encoding paused | Events buffered locally; Kafka cluster RF=3; retry on reconnect |
| Encoder service crash | Uploaded tracks stuck in PROCESSING | Kafka redelivers on consumer restart; job retried automatically |
| S3 region outage | Cache misses cannot be served | S3 geo-replication; CDN serves from cache for active tracks |
| PostgreSQL primary down | Track metadata writes fail | Failover to replica; promote to primary; < 60s downtime |
| DRM license service down | New offline downloads fail | Existing downloads still play (cached key); online streams unaffected |
Final Architecture
flowchart TD
Client[Mobile / Desktop / Web\nClients] --> AG[API Gateway\nAuth + Rate Limiting]
AG --> SS[Stream Service]
AG --> SRCH[Search Service]
AG --> REC[Recommendation Service]
AG --> PLS[Playlist Service]
AG --> USER[User Service]
AG --> NP[Now Playing Service]
SS --> CDN[CDN\n200+ Global PoPs]
CDN -->|cache miss| S3A[Processed S3\nAudio Files]
ART_UPLOAD[Artist Upload] --> S3R[Raw S3]
S3R --> KF[Kafka Cluster]
KF --> ENC[Encoder Workers\nFFmpeg]
ENC --> S3A
KF --> IDX[Search Indexer]
KF --> RC_PIPE[Recommendation\nPipeline Spark]
IDX --> ES[(Elasticsearch)]
SRCH --> ES
RC_PIPE --> FAISS[(FAISS\nEmbedding Store)]
REC --> FAISS
REC --> RDB[(Redis Cluster\nRec + Now Playing)]
PLS --> PDB[(Cassandra\nPlaylists + History)]
USER --> UDB[(PostgreSQL\nUsers + Tracks)]
NP --> RDB
NP --> WS[WebSocket\nConnected Devices]
Technology Stack
| Layer | Technology |
|---|---|
| API Gateway | NGINX / Envoy + custom auth |
| Stream Service | Java / Go |
| Encoder Service | Python + FFmpeg on Kubernetes Jobs |
| Message queue | Apache Kafka |
| Audio storage | Amazon S3 (multi-region) |
| CDN | Cloudflare / Akamai / Fastly |
| Track + user metadata | PostgreSQL (primary + read replicas) |
| Playlists + history | Apache Cassandra |
| Cache | Redis Cluster |
| Search | Elasticsearch (audio features + text) |
| Recommendations | Apache Spark + TensorFlow + FAISS |
| Real-time sync | WebSocket (Now Playing, Spotify Connect) |
| Audio codec | OGG Vorbis (primary), AAC (Apple devices) |
| Offline DRM | AES-256 device-bound encryption |
| Monitoring | Prometheus + Grafana + ELK |
| Deployment | Kubernetes (multi-region GCP + AWS) |
Key Trade-Offs
| Decision | Option A | Option B | Choice and Reason |
|---|---|---|---|
| Audio codec | MP3 (universal) | OGG Vorbis | OGG Vorbis — better quality per bit, royalty-free |
| CDN TTL for audio | Short (1 day) | Very long (1 year) | 1 year — audio files are immutable; maximize CDN hit rate |
| Recommendation timing | Real-time per request | Batch pre-computed weekly/daily | Batch — ML at 600M users cannot be real-time; weekly batch is correct |
| Play count storage | PostgreSQL row-level increment | Redis counter + async flush | Redis counter — 370K events/sec would deadlock PostgreSQL |
| Search | PostgreSQL full-text | Elasticsearch | Elasticsearch — audio features, fuzzy matching, NRT, autocomplete |
| Playlist storage | PostgreSQL | Cassandra | Cassandra — append-only wide rows; 600M users × many playlists each |
| Streaming format | Full file download | HTTP range requests (chunked) | Range requests — start in <250ms; no wasted bandwidth on skips |
| Offline protection | No DRM (trust user) | AES-256 device-bound DRM | DRM — subscription enforcement; prevents leaking Premium content |
Spotify vs YouTube vs Netflix
| Dimension | Spotify | YouTube | Netflix |
|---|---|---|---|
| Content type | Audio (3–10 MB) | Video (hundreds of MB) | Video (GBs) |
| CDN TTL | 1 year (fully immutable) | 30 days (video segments) | Proactive push nightly |
| CDN cache ratio | ~90% of catalog cacheable | Top 10% titles = 90% traffic | Pre-positioned at ISP level |
| Key challenge | Recommendation engine | Upload pipeline + fan-out | Delivery + resilience |
| Recommendation engine | Best in class — Discover Weekly | 70% from recommendations | 80% from recommendations |
| Social features | Follow, Now Playing, Collaborative | Subscribe, Like, Comment | Profiles only |
| Offline support | Yes (device-bound DRM) | Yes (YouTube Premium) | Yes (Widevine DRM) |
| Real-time features | Spotify Connect (multi-device sync) | None | Playback resume across devices |
Common Interview Mistakes
- ❌ Not mentioning OGG Vorbis — Spotify's codec choice is intentional and interview-relevant
- ❌ Designing full file downloads — Spotify uses HTTP range requests to start playback in 250ms
- ❌ Not explaining CDN TTL — audio files are immutable; 1-year TTL is correct and important
- ❌ Play counts as direct DB increments — 370K events/sec requires Redis counters
- ❌ No recommendation architecture — Discover Weekly is Spotify's most important feature
- ❌ Using PostgreSQL for playlists — 600M users × many playlists each = Cassandra territory
- ❌ Forgetting offline DRM — downloaded files must be subscription-enforced
- ❌ No Spotify Connect design — multi-device handoff is a common interview question
- ❌ Real-time recommendations — Discover Weekly is a weekly batch job, not real-time
- ❌ No search design — music search needs audio features + full-text, not just SQL LIKE
Interview Questions
- How does Spotify stream audio so quickly? What is the role of HTTP range requests?
- Why did Spotify choose OGG Vorbis over MP3?
- How does Discover Weekly work? What three types of signals does it use?
- What is collaborative filtering and how does Spotify apply it?
- How does Spotify analyze audio content to recommend sonically similar tracks?
- How do you handle 370,000 play events per second without overloading the database?
- How does Spotify Connect (multi-device handoff) work?
- How is offline listening implemented? How is the subscription enforced offline?
- Why is Cassandra used for playlists instead of PostgreSQL?
- How does the CDN strategy for audio differ from video streaming?
- How would you generate Discover Weekly for 600 million users every Sunday night?
- How does Spotify's search handle audio features like "tempo" and "energy"?
- What happens if the recommendation service goes down?
- How do you prevent a hot track from overloading a single Cassandra partition?
- How would you design the "What's Popular in Your City" regional trending feature?
Summary
| Concern | Solution |
|---|---|
| Audio streaming | HTTP range requests → CDN edge → start in 250ms; no full download |
| Audio encoding | OGG Vorbis at 4 quality tiers (96 / 160 / 320 Kbps + HiFi) per track |
| CDN strategy | 1-year TTL on immutable audio files; top 10M tracks fully cached at PoPs |
| Play count storage | Redis INCR → async flush to PostgreSQL every 30 seconds |
| Track + artist metadata | PostgreSQL — structured, ACID, complex joins |
| Playlists + history | Cassandra — wide rows, append-only, very high volume |
| Search | Elasticsearch — full-text + audio features + NRT indexing |
| Discover Weekly | Weekly batch: Spark ANN retrieval (500 candidates) → neural net ranking → top 30 |
| Daily Mixes | Daily batch: cluster user's listening history by genre/mood → blend known + new |
| Now Playing | Redis with 5-min TTL per user; pushed via WebSocket to friends and devices |
| Spotify Connect | Redis device state per user; new device reads position → seamless handoff |
| Offline DRM | AES-256 device-bound key; 30-day license TTL; unplayable if subscription lapses |
| Scale | Stateless services + CDN + Redis + Cassandra + Spark batch |
The core principle: Spotify is a recommendation and audio delivery problem. Audio is small and immutable — cache aggressively at CDN. The real engineering challenge is generating 600M personalized playlists every week. Discover Weekly is Spotify's most important system, not the streaming pipeline.