Design Spotify Music Streaming
FreeBuild a music streaming platform with audio playback, playlists, personalized recommendations, offline caching, and multi-device sync. This system design question tests your ability to create media-rich applications with real-time playback synchronization and intelligent content discovery.
**Difficulty:** Hard
**Estimated Time:** 40-50 minutes
## Problem Statement
Design a music streaming application that enables users to:
- Browse and discover music through personalized recommendations
- Play, pause, skip, and control audio playback
- Create and manage playlists
- Download songs for offline listening
- Sync playback across multiple devices
- Search and filter music by artist, album, genre, and song
This problem evaluates your understanding of audio streaming, playlist management, recommendation algorithms, offline caching, and cross-device synchronization.
## Requirements Analysis
### Core Functional Requirements
1. **Music Discovery**
- Personalized home feed with recommended music
- Browse by genre, mood, and activity
- Search with autocomplete
- View artist profiles and album pages
- Recently played and top tracks
2. **Audio Playback**
- Play/pause, next/previous track controls
- Seek to specific position in track
- Volume control and mute
- Shuffle and repeat modes
- Queue management
3. **Playlist Management**
- Create, edit, and delete playlists
- Add/remove songs from playlists
- Reorder tracks within playlists
- Collaborative playlists
- Liked songs collection
4. **Offline Mode**
- Download songs for offline playback
- Manage downloaded content
- Sync downloads across devices
- Offline playlist support
### Non-Functional Requirements
1. **Performance**
- Fast track start time (< 500ms)
- Smooth playback without interruptions
- Efficient audio buffering
- Quick playlist loading
2. **User Experience**
- Seamless cross-device playback
- Intuitive navigation
- Smooth animations
- Responsive on mobile devices
3. **Scalability**
- Support millions of tracks
- Handle concurrent playback sessions
- Efficient CDN for audio delivery
- Real-time sync capabilities
### Requirements Clarification Questions
1. **Audio Quality**: What quality options? (Normal, High, Very High)
2. **Offline Limits**: How many songs can be downloaded?
3. **Social Features**: Share playlists or make collaborative?
4. **Radio Mode**: Automatic playlists based on seeds?
5. **Lyrics**: Display synchronized lyrics?
## High-Level Architecture
```mermaid
graph TB
subgraph "Client Application"
HomePage[Home Page]
Player[Audio Player]
PlaylistManager[Playlist Manager]
Search[Search Interface]
end
subgraph "Audio Player"
AudioEngine[Audio Engine]
QueueManager[Queue Manager]
PlaybackController[Playback Controller]
OfflineManager[Offline Manager]
end
subgraph "State Management"
PlayerState[Player State]
PlaylistState[Playlist State]
RecommendationsState[Recommendations State]
OfflineCache[Offline Cache]
end
subgraph "Server APIs"
MusicAPI[Music API]
PlaybackAPI[Playback API]
RecommendationAPI[Recommendation API]
AudioCDN[Audio CDN]
SyncAPI[Device Sync API]
end
HomePage --> MusicAPI
Player --> AudioEngine
AudioEngine --> AudioCDN
PlaylistManager --> MusicAPI
RecommendationAPI --> HomePage
SyncAPI --> Player
OfflineManager --> OfflineCache
```
*Figure 1: Music Streaming Architecture*
## Audio Player Implementation
### Player State Management
```typescript
interface PlayerState {
currentTrack: Track | null;
queue: Track[];
currentIndex: number;
isPlaying: boolean;
currentTime: number;
duration: number;
volume: number;
isMuted: boolean;
isShuffled: boolean;
repeatMode: 'off' | 'one' | 'all';
isLoading: boolean;
error: string | null;
}
interface Track {
id: string;
title: string;
artist: Artist;
album: Album;
duration: number;
audioUrl: string;
previewUrl?: string;
coverArtUrl: string;
isDownloaded: boolean;
}
function useAudioPlayer() {
const audioRef = useRef<HTMLAudioElement | null>(null);
const [state, dispatch] = useReducer(playerReducer, initialState);
// Initialize audio element
useEffect(() => {
audioRef.current = new Audio();
audioRef.current.addEventListener('loadedmetadata', () => {
dispatch({
type: 'DURATION_CHANGE',
duration: audioRef.current?.duration || 0
});
});
audioRef.current.addEventListener('timeupdate', () => {
dispatch({
type: 'TIME_UPDATE',
time: audioRef.current?.currentTime || 0
});
});
audioRef.current.addEventListener('ended', () => {
handleTrackEnd();
});
audioRef.current.addEventListener('error', () => {
dispatch({ type: 'ERROR', error: 'Failed to load audio' });
});
return () => {
audioRef.current?.pause();
audioRef.current = null;
};
}, []);
// Sync audio element with state
useEffect(() => {
if (!audioRef.current) return;
if (state.isPlaying) {
audioRef.current.play();
} else {
audioRef.current.pause();
}
}, [state.isPlaying]);
useEffect(() => {
if (!audioRef.current || !state.currentTrack) return;
audioRef.current.src = state.currentTrack.audioUrl;
audioRef.current.load();
}, [state.currentTrack]);
useEffect(() => {
if (!audioRef.current) return;
audioRef.current.volume = state.isMuted ? 0 : state.volume;
}, [state.volume, state.isMuted]);
function playTrack(track: Track, queue?: Track[]) {
dispatch({ type: 'PLAY_TRACK', track, queue: queue || [] });
}
function togglePlayPause() {
dispatch({ type: 'TOGGLE_PLAY_PAUSE' });
}
function nextTrack() {
dispatch({ type: 'NEXT_TRACK' });
}
function previousTrack() {
dispatch({ type: 'PREVIOUS_TRACK' });
}
function seekTo(time: number) {
if (audioRef.current) {
audioRef.current.currentTime = time;
dispatch({ type: 'SEEK', time });
}
}
function handleTrackEnd() {
if (state.repeatMode === 'one') {
// Repeat current track
if (audioRef.current) {
audioRef.current.currentTime = 0;
audioRef.current.play();
}
} else if (state.repeatMode === 'all' && state.currentIndex === state.queue.length - 1) {
// Loop back to start
dispatch({ type: 'PLAY_TRACK', index: 0 });
} else if (state.currentIndex < state.queue.length - 1) {
// Play next track
nextTrack();
} else {
// Queue ended
dispatch({ type: 'PAUSE' });
}
}
return {
state,
playTrack,
togglePlayPause,
nextTrack,
previousTrack,
seekTo,
setVolume: (volume: number) => dispatch({ type: 'SET_VOLUME', volume }),
toggleMute: () => dispatch({ type: 'TOGGLE_MUTE' }),
toggleShuffle: () => dispatch({ type: 'TOGGLE_SHUFFLE' }),
setRepeatMode: (mode: PlayerState['repeatMode']) =>
dispatch({ type: 'SET_REPEAT_MODE', mode })
};
}
```
## Queue Management
### Shuffle Implementation
```typescript
function shuffleQueue(queue: Track[], currentIndex: number): Track[] {
const shuffled = [...queue];
const currentTrack = shuffled[currentIndex];
// Remove current track
shuffled.splice(currentIndex, 1);
// Fisher-Yates shuffle
for (let i = shuffled.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
// Insert current track at the beginning
shuffled.unshift(currentTrack);
return shuffled;
}
```
### Queue Operations
```typescript
function useQueue() {
const [queue, setQueue] = useState<Track[]>([]);
const [originalQueue, setOriginalQueue] = useState<Track[]>([]);
const [isShuffled, setIsShuffled] = useState(false);
function addToQueue(track: Track, position: 'next' | 'end' = 'end') {
setQueue(prev => {
if (position === 'next') {
return [track, ...prev];
}
return [...prev, track];
});
if (!isShuffled) {
setOriginalQueue(prev => [...prev, track]);
}
}
function removeFromQueue(trackId: string) {
setQueue(prev => prev.filter(t => t.id !== trackId));
setOriginalQueue(prev => prev.filter(t => t.id !== trackId));
}
function toggleShuffle() {
if (isShuffled) {
// Restore original order
setQueue(originalQueue);
} else {
// Shuffle queue
setQueue(shuffleQueue([...queue], 0));
}
setIsShuffled(!isShuffled);
}
function clearQueue() {
setQueue([]);
setOriginalQueue([]);
}
return {
queue,
addToQueue,
removeFromQueue,
toggleShuffle,
clearQueue,
isShuffled
};
}
```
## Playlist Management
### Playlist Component
```typescript
interface Playlist {
id: string;
name: string;
description?: string;
tracks: Track[];
owner: User;
isCollaborative: boolean;
coverImageUrl?: string;
}
function PlaylistView({ playlistId }: { playlistId: string }) {
const { data: playlist, isLoading } = usePlaylist(playlistId);
const { playTrack } = useAudioPlayer();
const [editing, setEditing] = useState(false);
function handlePlay() {
if (playlist && playlist.tracks.length > 0) {
playTrack(playlist.tracks[0], playlist.tracks);
}
}
function handleTrackReorder(sourceIndex: number, destinationIndex: number) {
// Reorder tracks in playlist
reorderPlaylistTracks(playlistId, sourceIndex, destinationIndex);
}
return (
<div className="playlist-view">
<PlaylistHeader playlist={playlist} onPlay={handlePlay} />
<div className="playlist-tracks">
<DragDropContext onDragEnd={handleTrackReorder}>
<Droppable droppableId="playlist">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{playlist?.tracks.map((track, index) => (
<Draggable key={track.id} draggableId={track.id} index={index}>
{(provided, snapshot) => (
<TrackRow
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
track={track}
index={index}
onPlay={() => playTrack(track, playlist.tracks.slice(index))}
onRemove={() => removeFromPlaylist(playlistId, track.id)}
/>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
</div>
);
}
```
## Offline Downloads
### Download Manager
```typescript
function useOfflineDownloads() {
const [downloadedTracks, setDownloadedTracks] = useState<Set<string>>(new Set());
const [downloadQueue, setDownloadQueue] = useState<string[]>([]);
const [downloading, setDownloading] = useState<Set<string>>(new Set());
async function downloadTrack(track: Track) {
if (downloadedTracks.has(track.id)) {
return; // Already downloaded
}
setDownloadQueue(prev => [...prev, track.id]);
setDownloading(prev => new Set(prev).add(track.id));
try {
// Fetch audio file
const response = await fetch(track.audioUrl);
const blob = await response.blob();
// Store in IndexedDB or Cache API
await storeTrackOffline(track.id, blob, track);
setDownloadedTracks(prev => new Set(prev).add(track.id));
setDownloading(prev => {
const next = new Set(prev);
next.delete(track.id);
return next;
});
} catch (error) {
console.error('Download failed:', error);
setDownloading(prev => {
const next = new Set(prev);
next.delete(track.id);
return next;
});
}
}
async function deleteDownload(trackId: string) {
await removeTrackOffline(trackId);
setDownloadedTracks(prev => {
const next = new Set(prev);
next.delete(trackId);
return next;
});
}
async function loadOfflineTrack(trackId: string): Promise<Blob | null> {
return await getTrackOffline(trackId);
}
return {
downloadedTracks,
downloadQueue,
downloading,
downloadTrack,
deleteDownload,
loadOfflineTrack
};
}
```
## Cross-Device Sync
### Playback Synchronization
```typescript
function usePlaybackSync() {
const { state: playerState } = useAudioPlayer();
const wsRef = useRef<WebSocket | null>(null);
useEffect(() => {
// Connect to sync WebSocket
wsRef.current = new WebSocket('wss://api.spotify.com/sync');
wsRef.current.onmessage = (event) => {
const syncData = JSON.parse(event.data);
switch (syncData.type) {
case 'PLAYBACK_STATE':
// Sync playback state from another device
syncPlaybackState(syncData.state);
break;
case 'QUEUE_UPDATE':
syncQueue(syncData.queue);
break;
}
};
return () => {
wsRef.current?.close();
};
}, []);
useEffect(() => {
// Broadcast state changes
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({
type: 'PLAYBACK_STATE_UPDATE',
state: {
currentTrack: playerState.currentTrack?.id,
isPlaying: playerState.isPlaying,
currentTime: playerState.currentTime
}
}));
}
}, [playerState.isPlaying, playerState.currentTrack, playerState.currentTime]);
}
```
## Optimizations
### 1. Audio Preloading
```typescript
function useAudioPreloading(queue: Track[]) {
useEffect(() => {
// Preload next 2-3 tracks in queue
const tracksToPreload = queue.slice(0, 3);
tracksToPreload.forEach(track => {
const audio = new Audio();
audio.preload = 'metadata';
audio.src = track.audioUrl;
// Load metadata
audio.addEventListener('loadedmetadata', () => {
audio.remove();
});
});
}, [queue]);
}
```
### 2. Progress Tracking
```typescript
function useProgressTracking(trackId: string, currentTime: number, duration: number) {
useEffect(() => {
const saveProgress = debounce(() => {
if (duration > 0 && currentTime > 5) {
savePlaybackProgress(trackId, currentTime);
}
}, 5000);
saveProgress();
return () => saveProgress.flush();
}, [trackId, currentTime, duration]);
}
```
## Accessibility
### Keyboard Controls
- Space: Play/Pause
- Left/Right arrows: Seek backward/forward
- Up/Down arrows: Volume control
- N: Next track
- P: Previous track
- S: Toggle shuffle
- R: Toggle repeat
## Testing Strategy
1. **Unit Tests**: Player reducer, queue operations
2. **Integration Tests**: Playback flow, playlist management
3. **E2E Tests**: Complete music playback journey
4. **Performance Tests**: Track start time, buffer management
5. **Accessibility Tests**: Keyboard navigation
## Summary
Designing a Spotify-style music streaming platform requires:
1. **Audio Player**: Robust playback control with queue management
2. **Playlists**: Efficient playlist operations and collaboration
3. **Offline Mode**: Download and cache management
4. **Cross-Device Sync**: Real-time playback synchronization
5. **Performance**: Fast track loading, efficient buffering
6. **Accessibility**: Full keyboard and screen reader support
This architecture provides a scalable, performant music streaming experience.
## Further Reading
- Web Audio API Documentation
- IndexedDB for Offline Storage
- WebSocket Real-time Sync
- Audio Streaming Best Practices