~learn.

Spotify, the whole design.

A music service that starts any of 100M+ tracks in under 200 ms for 777M monthly users, because most taps are served from the device or a CDN edge and nothing slow sits on the play path.

7 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-07 · Spotify · design sheethimanshukukreja.in/learn/sheets/bonus-07-spotify-streaming

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
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 store

Key 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 playback
DiagramRecommendations 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
  end
DiagramOffline 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
  end

Data 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

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#