Skip to main content

Spotify

A playlist has a fixed order; what plays next doesn't. Shuffle shouldn't mean physically reordering the playlist (now two listeners sharing it see different orders) or growing a NowPlayingQueue.next() method that branches on a boolean - it means the queue asks a pluggable strategy what's next and never finds out how that answer was decided.

Requirements

Functional

  • A user builds a playlist by adding songs pulled from albums and artists.
  • Playing a playlist loads its songs into a now-playing queue; the queue exposes the current track and supports skipping forward and backward.
  • Shuffle mode changes playback order without altering the playlist's own stored order.
  • Every song belongs to exactly one album, and every album to one artist.

Non-functional

  • Switching between sequential and shuffled playback must not require the queue to branch internally on a mode flag - the ordering logic is swappable.
  • Advancing to the next or previous track must be an O(1) pointer move, not a scan for the current track's position.

Design

NowPlayingQueue holds a fixed track list and a current index; it never decides what "next" means. That decision belongs to a PlaybackStrategy - SequentialPlaybackStrategy walks the list in order, ShufflePlaybackStrategy walks a precomputed permutation - so the queue's next()/previous() are one line each regardless of which strategy is plugged in.

UserPlaylistNowPlayingQueuePlaybackStrategyplay()1new NowPlayingQueue(songs, sequentialStrategy)2enableShuffle()3onEnable(songs.size())4next()5next(currentIndex)6
  1. 1Playing a playlist hands its song list to a fresh queue - the playlist itself is untouched.
  2. 2The queue starts with a default strategy; the playlist’s stored order is never copied elsewhere as a side effect.
  3. 3Turning on shuffle swaps the queue’s strategy - it does not touch playlist.songs.
  4. 4The permutation is computed once here, not recomputed on every skip.
  5. 5The queue asks the current strategy for the next index and never inspects it further.
  6. 6Sequential and shuffle strategies answer this identically from the queue’s point of view.

The playlist itself never changes when shuffle is toggled - only the queue's strategy does - which is what keeps a shared playlist's stored order stable no matter who's listening to it on shuffle.

Class diagram

«interface»PlaybackStrategy+ next(currentIndex): int+ previous(currentIndex): intArtist- name: stringAlbum- title: string- artist: ArtistSong- title: string- album: Album- durationSec: intPlaylist- owner: User- songs: List<Song>+ addSong(song)+ play(): NowPlayingQueueNowPlayingQueue- songs: List<Song>- currentIndex: int- strategy: PlaybackStrategy+ current(): Song+ next(): Song+ previous(): Song+ setStrategy(s)SequentialPlaybackStrategyShufflePlaybackStrategy- order: int[]User- id: string- name: string
implementsusescreates
NowPlayingQueue delegates ordering to PlaybackStrategy; the underlying Playlist order never changes.

Code

import java.util.*;
 
class Artist {
final String name;
 
Artist(String name) {
this.name = name;
}
}
 
class Album {
final String title;
final Artist artist;
 
Album(String title, Artist artist) {
this.title = title;
this.artist = artist;
}
}
 
class Song {
final String title;
final Album album;
final int durationSec;
 
Song(String title, Album album, int durationSec) {
this.title = title;
this.album = album;
this.durationSec = durationSec;
}
}
 
interface PlaybackStrategy {
void onEnable(int songCount);
int next(int currentIndex, int songCount);
int previous(int currentIndex, int songCount);
}
 
class SequentialPlaybackStrategy implements PlaybackStrategy {
public void onEnable(int songCount) {}
 
public int next(int currentIndex, int songCount) {
return Math.min(currentIndex + 1, songCount - 1);
}
 
public int previous(int currentIndex, int songCount) {
return Math.max(currentIndex - 1, 0);
}
}
 
class ShufflePlaybackStrategy implements PlaybackStrategy {
private List<Integer> order = new ArrayList<>();
 
public void onEnable(int songCount) {
order = new ArrayList<>();
for (int i = 0; i < songCount; i++) order.add(i);
Collections.shuffle(order);
}
 
public int next(int currentIndex, int songCount) {
int pos = order.indexOf(currentIndex);
return order.get(Math.min(pos + 1, order.size() - 1));
}
 
public int previous(int currentIndex, int songCount) {
int pos = order.indexOf(currentIndex);
return order.get(Math.max(pos - 1, 0));
}
}
 
class NowPlayingQueue {
private final List<Song> songs;
private int currentIndex = 0;
private PlaybackStrategy strategy;
 
NowPlayingQueue(List<Song> songs, PlaybackStrategy strategy) {
this.songs = songs;
this.strategy = strategy;
strategy.onEnable(songs.size());
}
 
void setStrategy(PlaybackStrategy strategy) {
this.strategy = strategy;
strategy.onEnable(songs.size());
}
 
Song current() {
return songs.get(currentIndex);
}
 
Song next() {
currentIndex = strategy.next(currentIndex, songs.size());
return current();
}
 
Song previous() {
currentIndex = strategy.previous(currentIndex, songs.size());
return current();
}
}
 
class Playlist {
final String id;
private final List<Song> songs = new ArrayList<>();
 
Playlist(String id) {
this.id = id;
}
 
void addSong(Song song) {
songs.add(song);
}
 
NowPlayingQueue play() {
return new NowPlayingQueue(new ArrayList<>(songs), new SequentialPlaybackStrategy());
}
}

Design decisions

  • Playback order is a PlaybackStrategy, not a boolean on the queue. A shuffled: bool field would still need an if somewhere to act on it; instead NowPlayingQueue.next() calls strategy.next(currentIndex) unconditionally; the strategy is the thing that changes, not the queue's code path.
  • Shuffle precomputes a permutation once, at the moment shuffle is turned on, not on every next() call. ShufflePlaybackStrategy shuffles an index list up front and walks it like the sequential strategy walks 0..n - so toggling shuffle mid-playlist doesn't mean re-deriving randomness every time a listener hits skip.
  • A Playlist stores songs in one order, period - the queue is a separate object that reads from it. Two listeners playing the same shared playlist on different modes (one shuffled, one sequential) each get their own NowPlayingQueue; neither can corrupt the playlist's stored order for the other.
  • Song -> Album -> Artist is a plain composition chain, not a shared registry class. There's no separate Catalog service here because nothing in this page's scope needs to search across all songs - only to know which album a song belongs to and which artist made it, which a direct reference answers in one hop.
  • What's missing for a real system: a content-based or collaborative recommendation engine, cross-device playback handoff, and offline-download state are all real Spotify features that would sit alongside PlaybackStrategy (recommendation could even be one more strategy implementation) rather than requiring a change to the queue's shape.
0%0 of 122 pages studied