Skip to content
System Design

Learn

Design a Music Streaming Service (LLD)

Low level design of a music streaming app: the playback state machine, the play queue with shuffle and repeat strategies, playlist modelling, offline downloads and scrobbling.

4 min readUpdated 2 Sept 2026

#LLD#State Machine#Strategy Pattern#Media

The design that looks trivial until you write it. Playback has more states than people expect, and shuffle plus repeat interact in ways that turn a naive implementation into tangled boolean logic. That interaction is what the interview is about.

Requirements

  • Browse and search songs, albums, artists, playlists.
  • Play, pause, skip, seek, previous; a queue with shuffle and repeat.
  • Create and share playlists; follow artists.
  • Offline downloads for premium users.
  • Recommendations and personalised mixes.

The domain model

Song       id, title, duration, artistIds, albumId, audioUrls{quality → url}
Album      id, title, artistId, releaseDate, songIds (ordered)
Artist     id, name, genres, followers
Playlist   id, owner, name, visibility, ordered entries, collaborators
User       id, subscription tier, library, preferences
Queue      the CURRENT playback list — distinct from any playlist
Player     playback state machine

The distinction that must be explicit: **a playlist is stored content; the queue is transient playback state. Playing a playlistcopies* it into the queue. If they are the same object, then skipping a track or adding "play next" mutates the saved playlist — a real bug in naive designs, and one interviewers probe for.

The playback state machine

IDLE → LOADING → PLAYING ⇄ PAUSED
          │         │  │
          │         │  └──→ BUFFERING ──→ PLAYING
          │         └──→ ENDED → (next track)
          └──→ ERROR

BUFFERING is the state people omit, and it is the one that matters: the player is trying to play but has no data. It is not paused — playback resumes automatically — so the UI and the logic must treat it distinctly.

java
class Player {
    private PlaybackState state = IDLE;
    private Song current;
    private Duration position;
    private final Queue queue;
    private final List<PlaybackListener> listeners;

    void play() { transition(PLAYING); }
    void pause() { transition(PAUSED); }

    void onTrackEnded() {
        scrobble(current, position);          // record the play BEFORE moving on
        queue.next().ifPresentOrElse(this::load, () -> transition(IDLE));
    }
}

Shuffle and repeat as strategies

The core of the problem. Encoded as booleans, "shuffle on, repeat one, press next" becomes a nest of conditionals. Make each an object:

java
interface PlayOrderStrategy {
    Optional<Song> next(QueueState q);
    Optional<Song> previous(QueueState q);
}

class SequentialOrder implements PlayOrderStrategy { … }

class ShuffleOrder implements PlayOrderStrategy {
    private final List<Integer> permutation;   // precomputed, not random-per-next
    // …
}

Shuffle is a permutation, not a random pick each time. Choosing randomly on every

next can repeat a song immediately and makes previous impossible to implement — you cannot go back to a track that was never recorded as coming before. Generate a shuffled order once (Fisher-Yates) and walk it; previous is then just walking backwards.

Repeat composes on top rather than being a second flag:

java
enum RepeatMode { OFF, REPEAT_ALL, REPEAT_ONE }

REPEAT_ONE short-circuits next to return the current song; REPEAT_ALL wraps the index at the end — and, if shuffling, reshuffles for the new pass so the second listen is not identical. Keeping order and repeat as separate concerns is what stops the combinatorial explosion.

Playlists

playlist_entries  playlist_id, position, song_id, added_by, added_at

A song may appear twice in a playlist, so (playlist_id, song_id) cannot be the key — position is part of the identity. That also makes reordering a real operation: renumbering every row on each drag is O(n), so use fractional or gapped positions (100, 200, 300; insert at 150) and renumber only occasionally.

Collaborative playlists need concurrent-edit handling, which is the same problem as collaborative editing in miniature — though last-writer-wins per entry is usually acceptable here.

Streaming and offline

Audio is served as segmented, adaptive-quality streams, like video but far smaller. The client prefetches the next track while the current one plays, so a skip is instant — the same prefetch idea as TikTok.

Downloads for offline use are DRM-protected files with a licence that expires, requiring a periodic online check. The model needs a DownloadState per song per device, and a cache eviction policy when storage runs out.

Scrobbling

Play events drive royalties and recommendations, so the rules must be explicit: a play counts after a threshold (Spotify uses 30 seconds), a skip before it does not, and events are batched and queued rather than sent per track — and buffered while offline. Getting the "what counts as a play" rule stated is the mark of someone who has thought about the domain.

Patterns worth naming

  • State — the player.
  • Strategy — play order, audio quality selection, recommendations.
  • Observer — UI, scrobbler and cache all reacting to playback events.
  • Command — playback controls, which makes them queueable and remotely invokable

(the basis of "control playback on another device").

  • Composite — treating songs, albums and playlists uniformly as playable sources.

Edge cases to handle

  • A song removed from the catalogue while in someone's playlist or queue → tombstone it,

keep the entry, mark it unavailable.

  • Network drops mid-song → BUFFERING, then fall back to a lower quality.
  • Regional licensing — a track available in one country and not another.
  • Playback on multiple devices — a session must transfer, not duplicate.
  • Adding to a queue that is currently shuffling.

Extensions to be ready for

  • Crossfade and gapless playback — needs the next track decoded early.
  • Collaborative sessions — several people adding to one live queue.
  • Podcasts — resume position per episode, variable speed.
  • Radio / autoplay — an endless queue generated by recommendations.
  • Lyrics sync — timestamped lines streamed alongside audio.

Track this problem on the System Design sheet.

Frequently asked

How should shuffle be implemented in a music player?

As a precomputed permutation of the queue, not a random choice on each next. Choosing randomly per skip can replay a song immediately and makes previous impossible, because there is no recorded order to walk back through. Generate a shuffled order once with Fisher-Yates, walk it forwards and backwards, and reshuffle when repeat-all wraps to a new pass.

What is the difference between a playlist and a play queue?

A playlist is stored, shareable content; the queue is transient playback state. Playing a playlist copies its songs into the queue, so skipping tracks or adding play-next entries modifies only the queue. If the two are the same object, ordinary playback actions silently mutate the user's saved playlist.

Why does the player need a BUFFERING state?

Because it is behaviourally different from both playing and paused: the player is trying to play but has no data, and it will resume on its own once data arrives. Collapsing it into PAUSED means the UI shows a paused player that unexpectedly restarts, and the logic cannot distinguish a user-initiated pause from a network stall.

Related