FSD Frontend System Design

Part VI β€” The Blueprint Β· Lesson 11.6

HLD – Music Streaming (Spotify)

In one lineThe player must survive navigation, so it lives above the router β€” everything else follows from that.

Where you've seen itClicking through albums, artists, and search while playback never pauses for even a frame.

  • Time18 min
  • DiagramPlayer above the route tree + queue state machine
  • Chapter11 Β· High Level Design

Diagram

diagram
1–2 Β· SCOPE AND SCALE

  in scope   browse Β· search Β· album/playlist Β· player Β· queue Β· offline
  out        podcasts Β· social Β· ads Β· artist tools
  scale      600M users Β· tracks ~3MB at 160kbps
             sessions measured in hours, not minutes
diagram
3 Β· THE ARCHITECTURAL DECISION β€” player OUTSIDE the router

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ App shell ─────────────────────────┐
  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
  β”‚  β”‚  <Router>   ← everything here unmounts on navigate    β”‚ β”‚
  β”‚  β”‚    /browse   /album/:id   /search   /playlist/:id     β”‚ β”‚
  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
  β”‚  β”‚  <Player>   ← single <audio> element, NEVER unmounts  β”‚ β”‚
  β”‚  β”‚  play/pause Β· seek Β· queue Β· volume                   β”‚ β”‚
  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  Put <audio> inside a route and every navigation restarts the song.
  This single decision is most of the answer.
diagram
6a Β· DEEP DIVE β€” QUEUE STATE MACHINE

  [idle] ──play(track)──► [loading] ──canplay──► [playing]
                              β”‚                    β”‚  β”‚
                              β”‚              pause β”‚  β”‚ ended
                              β–Ό                    β–Ό  β–Ό
                          [error]              [paused] [next?]
                         retry / skip                     β”‚
                                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”
                                          queue empty          more
                                              β”‚                  β”‚
                                           [idle]           [loading]

  queue = { context: 'album:42', items: [...],
            index: 3, shuffle: false, repeat: 'off'|'one'|'all' }
  Shuffle stores a shuffled ORDER, not a shuffled array β€” so
  turning it off restores the original sequence.
diagram
6b Β· DEEP DIVE β€” GAPLESS PLAYBACK AND PREFETCH

  now playing  β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘  80%
                                     β”‚
                                     └─► start fetching next track
                                         (two <audio> elements,
                                          swap on ended β†’ no gap)

  offline (9.1)  downloaded tracks β†’ IndexedDB (6.5)
                 served by the service worker on request
                 licence/expiry checked before playback

How it works

  1. Single audio element above the router, driven by a global player store (10.5) β€” routes only dispatch intents.
  2. Queue as explicit state: context, items, index, shuffle order, repeat mode.
  3. Prefetch the next track at ~80% and use a second audio element for gapless transitions.
  4. Media Session API wires lock-screen controls, hardware keys, and metadata.
  5. Offline stores downloaded audio in IndexedDB and serves it through the service worker.

Trade-offs

ChoiceCost
Player outside routerApp shell can't be fully route-code-split
Two audio elementsMore memory; gapless transitions
Prefetch next trackWasted data if the user skips
Offline downloadsStorage quota, licensing, eviction handling

Interview angle

"Music stops when the user navigates. Why, and how do you fix it?"

  • The <audio> element is inside a route, so navigation unmounts and recreates it.
  • Hoist a single player instance above the router and drive it from a global store; routes dispatch play/pause intents only.
  • Then add Media Session so hardware and lock-screen controls work, which is expected behaviour for anything audio.

Recap

  • The player lives above the router β€” that's the design.
  • Queue is explicit state: context, index, shuffle order, repeat mode.
  • Prefetch at ~80% and swap audio elements for truly gapless playback.