At a glance#
| Number | Value | What it drives |
|---|---|---|
| Users | 777M MAU · 50M concurrent at peak | 300M premium, 184 markets |
| Streams | 5B a day · ~175K / s at peak | ~58K / s average, 3× peak |
| Tap to first audio | < 200 ms | client cache, prefetch, ~30 ms authorisation |
| Peak egress | ~5.5 Tbps · 20 PB a day | ~4 MB a song; a bought CDN is enough |
| Catalogue | 100M+ tracks · 2.5 PB | ~25 MB a track across all encodings |
| Events | 500B a day · ~250 TB a day | ~10 PB a year; stream + batch paths |
| API | ~500K req / s · p99 < 50 ms | 2,000+ services, search < 100 ms |
Architecture#
DiagramSpotify on one page: authorise once, audio from the edge
%% title: Spotify on one page: authorise once, audio from the edge
flowchart TD
CL@{ icon: "sd:phone", label: "Clients · iOS · Android · desktop · web<br/>local cache 1–10 GB · prefetch · offline" } --> GW@{ icon: "sd:gateway", label: "API gateway<br/>auth · rate limit · routing" }
GW ==> ST["Stream service<br/>tier · country rights · signed URL, 1 h<br/>prefetch hints"]:::hot
GW --> MS["2,000+ services<br/>user · playlist · search · reco · ads<br/>every call behind a breaker"]:::core
MS --> PG@{ icon: "tech:postgresql", label: "Postgres<br/>users · playlists · library" }
MS --> EL@{ icon: "tech:elasticsearch", label: "Elastic<br/>search · 100M+ tracks" }
MS --> RD@{ icon: "tech:redis", label: "Redis<br/>cache" }
CL ==>|"signed URL"| CDN@{ icon: "sd:cdn", label: "CDN edge · Fastly / Akamai<br/>token checked at edge<br/>99 %+ hit for popular" }
CDN -.->|"miss · long tail"| GCS@{ icon: "tech:gcs", label: "GCS origin<br/>all tracks · 2.5 PB" }
CL -.->|"play · skip events, async"| PS@{ icon: "tech:pubsub", label: "Pub/Sub<br/>500B events a day" }
PS --> AN@{ icon: "tech:dataflow", label: "Dataflow · trending, taste updates<br/>Dataproc · ML, Discover Weekly" }
AN --> BT@{ icon: "tech:bigtable", label: "Bigtable<br/>reco features · taste vectors" }
class CL client
class GW edge
class CDN,RD cache
class PG,EL,BT,GCS store
class PS queue
class AN service- The play path is gateway → stream service → CDN. Recommendations, ads and analytics are off it, so they can fail without stopping music.
- The backend authorises once per track; after that the edge checks the signed token itself and serves bytes.
DiagramWhere the audio for one tap comes from, fastest first
%% title: Where the audio for one tap comes from, fastest first
flowchart TD
TAP["Tap play"]:::service --> L1{"On the device?<br/>recent · prefetched next · offline"}
L1 -->|"yes · most taps"| DEV["Plays at once<br/>no network"]:::ok
L1 -->|"no"| AUTH["Stream service<br/>rights + catalog in parallel · ~30 ms"]:::hot
AUTH -->|"not licensed here"| NO["No signed URL<br/>no stream"]:::bad
AUTH -->|"signed URL"| L2{"At the CDN edge?<br/>top 20 % always cached"}
L2 -->|"hit"| EDGE["Stream from edge<br/>start low, then upgrade"]:::ok
L2 -->|"miss"| ORI@{ icon: "tech:gcs", label: "GCS origin<br/>slower first play" }
class ORI storeKey flows#
DiagramTap to play: one authorisation, bytes from the edge
%% title: Tap to play: one authorisation, bytes from the edge
sequenceDiagram
autonumber
participant C as Client
participant G as API gateway
participant S as Stream service
participant E as CDN edge
participant O as GCS origin
participant P as Pub/Sub
C->>G: Play track · session
G->>S: Authorise
Note over S: tier · country licence · catalog, in parallel
S-->>C: Signed URL (1 h) · prefetch URLs for next tracks
C->>E: GET audio with token
alt popular track
E-->>C: Audio from cache · token checked at edge
else long-tail miss
E->>O: Fetch
O-->>E: Audio
E-->>C: Audio
end
C-)P: Play event (async)
Note over C,P: under 200 ms to first audio · events never block playbackDiagramRecommendations fail: music keeps playing
%% title: Recommendations fail: music keeps playing
sequenceDiagram
autonumber
participant C as Client
participant H as Home service
participant R as Reco service
participant S as Stream service
C->>H: Home screen
H->>R: Personal rows
R--xH: errors (5 in a row)
Note over H: breaker OPEN · calls go straight to fallback
H-->>C: Popular and editorial rows
C->>S: Play track
S-->>C: Signed URL · playback unaffected
alt after 30 s · HALF_OPEN
H->>R: Trial call
R-->>H: ok · breaker CLOSED
else still failing
Note over H: OPEN again · keep the fallback
endDiagramOffline plays upload late: count each play once
%% title: Offline plays upload late: count each play once
sequenceDiagram
autonumber
participant C as Phone
participant P as Pub/Sub
participant D as Dedup + counter
participant F as Fraud check
Note over C: offline on a flight · play ID made at play start
C->>P: 30 play events, 6 h later (play ID · event time)
P->>D: Deliver (at-least-once)
P->>D: Redeliver some
Note over D: keep one per play ID · window by event time
alt played long enough, e.g. 30 s
D->>F: Candidate counts
F-->>D: drop loops and bot accounts
Note over D: counted in the period it was played
else early skip
Note over D: not paid · −0.5 taste signal
endData model#
DiagramCore entities
%% title: Core entities
classDiagram
class User {
+userId : string · partition key
+tier : free / premium
+country : string
}
class Track {
+trackId : string
+durationMs : int
+markets : list · licence per country
}
class Encoding {
+codec : Ogg Vorbis / AAC / FLAC
+bitrate : 24 to 320 kbps
+gcsPath : string
}
class Playlist {
+playlistId : string
+ownerId : string
+trackIds : list
}
class PlayEvent {
+playId : UUID · dedup key
+userId : string
+trackId : string
+eventTime : timestamp · not arrival
+playedMs : int
+endReason : completed / skipped / error
}
class TasteVector {
+userId : string
+embedding : 128 floats
}
User "1" --> "many" Playlist : owns
Playlist "many" --> "many" Track : lists
Track "1" --> "many" Encoding : stored as
User "1" --> "many" PlayEvent : emits
User "1" --> "1" TasteVector : learned from plays- User data partitions by
user_id(~50 KB a user, ~39 TB total) and is strongly consistent; events partition by time (~500 B each, ~250 TB a day). - Audio is ~25 MB a track across encodings, 2.5 PB in GCS; vectors are 777M users and 100M songs × 128 dims.
Lifecycle#
DiagramA play event, from tap to royalty count
%% title: A play event, from tap to royalty count stateDiagram-v2 [*] --> STARTED: play ID created on device STARTED --> SENT: async to Pub/Sub STARTED --> HELD_OFFLINE: no network HELD_OFFLINE --> SENT: uploaded later SENT --> DUPLICATE: play ID already seen SENT --> TOO_LATE: past lateness window SENT --> DEDUPED: first copy kept DEDUPED --> NOT_COUNTED: below minimum listen DEDUPED --> FLAGGED: fraud anomaly DEDUPED --> COUNTED: by event time COUNTED --> FINAL: report finalised FINAL --> [*] DUPLICATE --> [*] TOO_LATE --> [*] NOT_COUNTED --> [*] FLAGGED --> [*] class FINAL ok class DUPLICATE,TOO_LATE,NOT_COUNTED,FLAGGED bad class HELD_OFFLINE queue
Decisions#
| Decision | Chose | Over | Because |
|---|---|---|---|
| Play authorisation | Signed URL, 1 h, checked at edge | Backend check per request | backend off the byte path, leaked links expire |
| Delivery | Bought CDN (Fastly / Akamai) | Own CDN | ~5.5 Tbps peak, a tenth of video |
| Latency | Client → edge → origin, prefetch next tracks | Fetch on tap | most taps never touch the network |
| Events | Async to Pub/Sub | Write before play | analytics never slows the first note |
| Consistency | Strong for playlists, eventual for recs | One model for all | a new song must show on your other device; recs can lag |
| Recommendations | CF + content + audio CNN, 20 % exploration | Collaborative filtering only | cold start for new songs and users, no filter bubble |
| Counting plays | Play ID + event-time windows | Count arrivals | at-least-once delivery becomes exactly-once counts |
When it breaks#
| Failure | What users see | Contain it with |
|---|---|---|
| Recommendation service fails | popular rows; music plays | breaker + fallback |
| Long-tail track not at edge | slower first play | origin fetch, prefetch the queue |
| Network drops (tunnel) | keeps playing, maybe lower quality | 20+ s buffer, prefetch, offline cache |
| Huge album drop | nothing, if prepared | pre-cache on all CDN nodes a week early, API 2–3×, rate limits |
| Event pipeline lag | staler stats and recs only | async path, alert on Pub/Sub backlog > 10M |
| Duplicate or bot plays | wrong royalties | dedup on play ID, fraud check before reports are final |
Say it in the interview#
- "Recommendations can fail; playback must not." That one rule decides what sits on the play path.
- Audio is light, latency is hard: ~5.5 Tbps at peak is a tenth of video, so a bought CDN works; the 200 ms start is the problem.
- Authorise once, sign the URL: rights and licence checked once, the edge verifies the token, the backend never touches bytes.
- The fastest fetch is no fetch: prefetch the next tracks so skips and gapless playback come from the device.
- Fire-and-forget events: play and skip events go async; a 40-minute backlog makes recs stale, not music late.
- Three models beat one: audio analysis places a song with zero plays; 6 of 30 Discover Weekly tracks are exploration.
- Exactly-once is a key, not a transport: a play ID made at play start, counted by event time, fraud filtered before payout.