System Design ( Day - 54 )

Design Music Player application
Functional Requirements
1. User can play, pause songs
2. User can create a playlist and add songs to playlist
2.1 Play entire playlist( Sequential manner, random manner etc… )
3. App should support multi output devices ( Bluetooth, speaker, wired speaker, headphones etc… )
Non Functional requirements.
1. Entire design should be easily scalable.
2. New Features( new output devices, new ways to play song from playlist ) can be easily
Building a fully-featured music app means juggling playlists, audio engines, output devices, and playback strategies. To keep our code clean, extensible, and maintainable, we can combine several classic patterns:
Singleton – one global instance for managers
Strategy – swap playback algorithms on-the-fly
Adapter – unify disparate speaker APIs
Factory – create device adapters by type
Facade – expose a simple “play this song” API
Let’s explore each layer of our system.
1️⃣ Singletons: Centralized Managers
We want exactly one global:
MusicPlayerApplication
MusicPlayerFacade
PlaylistManager
PlaybackStrategyManager
DeviceManager
Each uses the Singleton pattern so every component gets the same instance when they call, for example, PlaylistManager.getInstance()
.
2️⃣ Strategy: Flexible Playback Order
Users expect sequential, shuffle, or custom playback. We define:
interface PlayStrategy {
void setPlaylist(Playlist p);
Song next();
Song previous();
void addToNext(Song s);
}
Concrete strategies like SequentialPlayStrategy
, RandomPlayStrategy
, and CustomPlayStrategy
implement this interface. The PlaybackStrategyManager
holds them and switches based on user choice.
3️⃣ Adapter: Supporting Multiple Speakers
Your audio engine must treat a Bluetooth speaker, a wired speaker, or headphones the same:
interface IAudioOutputDevice {
void playAudio(Song song);
}
Each device has its own SDK:
BluetoothSpeakerAPI.playSoundViaBluetooth(data)
WiredSpeakerAPI.playSoundViaCable(data)
HeadphonesAPI.playSound(data)
We write adapters:
class BluetoothSpeakerAdapter implements IAudioOutputDevice {
private BluetoothSpeakerAPI api;
public void playAudio(Song song) {
api.playSoundViaBluetooth(song.toBytes());
}
}
And similarly for wired speakers and headphones.
4️⃣ Factory: Creating Adapters by Type
Instead of if–else
everywhere, DeviceManager
calls a DeviceFactory
:
class AudioDeviceFactory {
static IAudioOutputDevice getDevice(DeviceType type) {
switch(type) {
case BLUETOOTH: return new BluetoothSpeakerAdapter();
case WIRED: return new WiredSpeakerAdapter();
case HEADPHONES:return new HeadphonesAdapter();
// …
}
}
}
Now connecting a new device is as simple as changing the factory mapping.
5️⃣ Facade: One API to Rule Them All
At the top sits MusicPlayerFacade
, our single entry point:
public class MusicPlayerFacade {
private AudioEngine engine;
private PlaylistManager playlists;
private PlayStrategyManager strategies;
private DeviceManager devices;
public void connectDevice(DeviceType type) { … }
public void createPlaylist(String name) { … }
public void addSong(String list, Song s) { … }
public void setStrategy(StrategyType t) { … }
public void play() {
Song s = strategies.getCurrent().next();
engine.play(devices.getCurrent(), s);
}
public void pause() { engine.pause(); }
public void next() { /* … */ }
public void previous() { /* … */ }
}
Clients call only these facade methods and never worry about underlying adapters, factories, or strategy switches.
🚀 Why This Architecture Rocks
Single Responsibility: Each class does one thing
Open/Closed: Add new speaker types, strategies, or playlist rules without touching existing code
Low Coupling: Facade shields clients from inner complexity
High Cohesion: Related functionality lives together
Subscribe to my newsletter
Read articles from Manoj Kumar directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
