Design Spotify
OOP design for a music streaming platform covering songs, playlists, albums, artist profiles, playback queue, shuffle algorithms, and subscription tiers.
The Problem
Your company runs a music streaming service with 50 million users. The player is a monolithic PlayerController class that has grown to 4,000 lines. Shuffle is a random index pick that frequently replays the same song twice in a row. The playback queue is an ArrayList that rebuilds from scratch every time a user adds a song. Premium users hear ads because the subscription check is scattered across 20 different methods with duplicated if (user.isPremium()) guards. Last week a developer broke the repeat-one feature while fixing shuffle, because both are interleaved in the same method.
Music streaming platforms are hard LLD problems because they blend media playback with social features, content management, and subscription gating. Users browse songs, albums, and artists. They create and share playlists. They control playback with play, pause, skip, and seek. They shuffle, repeat, and queue songs. Each feature seems simple on its own, but the challenge is separating playback control from queue management, keeping shuffle algorithms swappable, and cleanly gating features by subscription tier without polluting every method.
Design the core classes for a music streaming platform that handles songs with metadata, albums and artist profiles, user-created playlists with collaboration, a playback queue with shuffle and repeat modes, subscription tiers controlling feature access, playback event notifications, and search across the music catalog.
Requirements
Clarifying Questions
Before jumping into class design, ask questions to turn the vague prompt into a concrete specification. Cover four areas: core actions, error handling, boundaries, and future extensions.
You: "How does playback work? Does the user always play from a playlist, or can they play individual songs and albums too?"
Interviewer: "All three. A user can play a single song, an entire album, or a playlist. When they start playback, the songs load into a playback queue. The user can also manually add songs to the queue at any point."
So we need a PlaybackQueue that accepts songs from any source: a single song, an album track list, or a playlist. The queue is the single source of truth for what plays next.
You: "What shuffle behavior do we need? Just basic random, or something smarter?"
Interviewer: "Support multiple shuffle algorithms. Basic Fisher-Yates random for simplicity. A weighted shuffle that prioritizes songs the user listens to more. And an artist-spread shuffle that avoids playing two songs by the same artist back-to-back."
Three interchangeable algorithms, same input (list of songs), same output (shuffled list). Classic Strategy pattern.
You: "What repeat modes should we support?"
Interviewer: "Three modes: OFF (stop at end of queue), REPEAT_ONE (loop the current song), and REPEAT_ALL (restart the queue from the beginning when it ends). The user toggles between them."
Repeat mode changes how next() behaves in the queue. We store it as an enum and check it during playback advancement.
You: "How do subscription tiers work? What is gated for free users?"
Interviewer: "Two tiers: Free and Premium. Free users hear ads between songs, can only shuffle-play on mobile (no on-demand pick), and get standard audio quality. Premium users get on-demand playback, no ads, offline downloads, and high-quality audio. Design it so adding tiers later is straightforward."
Subscription determines feature access. Instead of scattering if (premium) checks everywhere, we can model each tier as an object that answers feature queries: canPlayOnDemand(), canDownload(), getAudioQuality().
You: "Can multiple users collaborate on a playlist?"
Interviewer: "Yes. A playlist has one owner and zero or more collaborators. Both owner and collaborators can add and remove songs. Only the owner can delete the playlist or manage collaborators."
Collaborative playlists need an access control check: is this user the owner OR a collaborator? The Playlist class tracks both roles.
You: "Should we support notifications for playback events or new releases?"
Interviewer: "Yes. Notify when: a song starts playing, a song finishes, a playlist is updated by a collaborator, and an artist the user follows releases new music. Make it extensible for new event types."
Observer pattern. Playback events and social events both flow through a notification system. Listeners subscribe to event types they care about.
You: "What about search? Full-text across everything, or scoped searches?"
Interviewer: "Search returns songs, albums, artists, and playlists. Rank results by relevance. The actual search algorithm is out of scope, but model the search result types."
We need a SearchResult wrapper that holds mixed types. The ranking algorithm lives behind an interface for future swapping.
You: "Are podcasts, lyrics, or equalizer settings in scope?"
Interviewer: "Not for the initial design. But mention how you would extend for those in the extensibility section."
Good. Out of scope for now, but the design should have natural extension points.
Perfect. You have clarified scope. The core system handles songs with metadata, albums with track lists, artist profiles, playlists with collaboration, a playback queue with shuffle and repeat, subscription-gated features, playback event notifications, and catalog search.
Final Requirements
Functional Requirements:
- Users browse songs, albums, artists, and playlists in a searchable catalog.
- Users create playlists and optionally invite collaborators who can add or remove songs.
- Playing a song, album, or playlist loads tracks into a playback queue.
- Users control playback: play, pause, skip next, skip previous, seek.
- Shuffle uses swappable algorithms (Fisher-Yates, weighted, artist-spread).
- Repeat supports three modes: OFF, REPEAT_ONE, REPEAT_ALL.
- Subscription tier (Free/Premium) gates features: on-demand play, ads, audio quality, offline.
- Observers receive notifications for playback events and new releases.
Non-Functional Requirements:
- Thread safety for concurrent playlist edits by collaborators.
- Extensibility for new shuffle algorithms, subscription tiers, and notification types.
- Clean separation between playback control, queue management, and subscription logic.
Out of Scope:
- Audio streaming and codec handling
- UI rendering
- Persistence and database
- Podcast support (covered in Extensibility)
- Lyrics display (covered in Extensibility)
- Social features like shared listening sessions (covered in Extensibility)
30-Second Design Summary
Separate catalog content from runtime playback: Song, Album, and Playlist describe what exists, while PlaybackQueue decides what comes next and PlaybackState tracks what is happening now. PlaybackService coordinates queue operations, subscription feature checks, and events; shuffle and recommendation algorithms are strategies. The queue owns a user's temporary order so shuffling one listener never mutates the shared playlist.
5-Minute Walkthrough
- Set the scope. The core covers catalog/playlists, collaborative edits, queue loading, playback controls, shuffle, repeat, subscription gates, and playback observers. Streaming codecs, persistence, UI, podcasts, lyrics, and shared sessions are extensions.
- Separate content from runtime state. A playlist is an ordered source list; a playback queue is a per-user working copy; playback state holds current song, progress, and playing/paused status.
- Load and play. Resolve a song/album/playlist into the queue, apply the selected shuffle strategy if enabled, check subscription permissions, then advance the cursor and emit playback events.
- Handle repeat and history.
REPEAT_ONE,REPEAT_ALL, andOFFalter cursor behavior without changing the source playlist. Observers receive completed/started events for history, analytics, and notifications. - Extend through policies. New playable media, shuffle algorithms, subscription tiers, and event listeners should implement focused contracts. Collaborative playlist edits need a concurrency boundary separate from a listener's playback queue.
Example Inputs and Outputs
Scenario 1: Shuffle play a playlist
- Input: User selects playlist "Morning Vibes" (10 songs) and taps shuffle play.
- Expected: Songs load into the queue in a shuffled order using the active shuffle algorithm. Playback starts with the first shuffled song. No two consecutive songs are by the same artist (if using artist-spread shuffle).
- Why: Validates queue loading, shuffle strategy, and playback start.
Scenario 2: Premium user queues a song mid-playback
- Input: While listening to Song A, a Premium user adds Song X to the queue. Song B was next.
- Expected: Queue becomes [..., Song A (playing), Song X, Song B, ...]. After Song A ends, Song X plays, then Song B resumes its position.
- Why: Validates manual queue insertion without disrupting existing order.
Scenario 3: Free user tries on-demand play on mobile
- Input: A Free-tier user taps a specific song on mobile.
- Expected: System rejects on-demand play. Instead, it starts shuffle play of the song's album or playlist context.
- Why: Validates subscription tier gating for feature access.
Try It Yourself
Try it yourself
Before reading the solution, spend 20 minutes sketching your own class diagram. Focus on how the playback queue interacts with shuffle and repeat modes. Ask yourself: where does the shuffle algorithm live? Who owns the repeat mode state? How does subscription tier gate features without polluting every method?
Step 1: Identify Core Entities
Start by asking: what are the main "things" in this problem? Look at your requirements and pull out the nouns. Each noun that has its own state or behavior becomes a candidate class.
A common mistake is merging playback control into the Song or Playlist class. Playback is a runtime concept (what is currently playing, what comes next). Songs and playlists are content that exists whether anyone is playing them or not. Keeping them separate respects SRP.
| Entity | Responsibility | Key attributes |
|---|---|---|
| Song | Immutable music metadata. No playback logic. | title, artist, album, duration, genre |
| Album | Ordered collection of songs by one artist. | title, artist, songs, releaseDate |
| Artist | Profile for a music creator. Owns albums. | name, bio, albums, followers |
| Playlist | User-curated song list with collaboration. | name, owner, collaborators, songs |
| User | Account holder. Owns playlists, follows artists. | username, email, subscription, playlists, followedArtists, playHistory |
| PlaybackQueue | Ordered list of songs to play. Manages current position. | songs, currentIndex, repeatMode |
| PlaybackState | Runtime state of the player: playing, paused, current song, progress. | currentSong, isPlaying, progressMs |
| Subscription | Feature gate. Determines what a user can and cannot do. | tier, canPlayOnDemand, canDownload, audioQuality |
| Genre | Category tag for songs. | name |
| PlayHistory | Record of what the user has listened to. | entries (song + timestamp) |
| SearchResult | Container for mixed search results. | songs, albums, artists, playlists |
Notice we separated PlaybackQueue from PlaybackState. The queue knows WHAT comes next. The state knows the current playback status (playing/paused, progress bar position). Merging them forces the queue to carry runtime UI concerns it should not own.
Step 2: Define Relationships and Class Design
Class Diagram
User
The User is the entry point for all interactions. Every action (create playlist, play song, follow artist) starts from a User.
Deriving state from requirements:
| Requirement | What User must track |
|---|---|
| "Users create playlists" | List of owned playlists |
| "Subscription tier gates features" | Current subscription |
| "Users follow artists for new releases" | Set of followed artists |
| "Support play history for recommendations" | Play history entries |
This gives us:
User:
username: String
email: String
subscription: Subscription
playlists: List<Playlist>
followedArtists: Set<Artist>
playHistory: PlayHistory
Deriving methods from needs:
| Need from requirements | Method |
|---|---|
| "Users create playlists" | createPlaylist(name): Playlist |
| "Users follow artists" | followArtist(artist): void |
| "Track listening history" | recordPlay(song): void |
| "Subscription gates features" | getSubscription(): Subscription |
PlaybackQueue
The PlaybackQueue is the most complex entity. It manages what plays next, handles shuffle and repeat, and supports manual insertions.
Deriving state from requirements:
| Requirement | What PlaybackQueue must track |
|---|---|
| "Songs load into a playback queue" | Ordered list of songs |
| "Skip next, skip previous" | Current position index |
| "Repeat OFF/ONE/ALL" | Active repeat mode |
| "Shuffle uses swappable algorithms" | Reference to shuffle strategy |
This gives us:
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.