Machine Coding Problem

Music Playlist Manager

macoAllmediadoubly-linked-list-logic
Commonly Asked By:SpotifyAppleAmazon

Requirements & System Scope

Functional Scope (In-Scope)

  • Constant-time O(1) Operations: Supports adding, removing, and re-linking list nodes in constant time using combined DLL and Map indexing.
  • Repeat Playback State Machine: Handles standard traversal variants (None, Single-track repeat, Full-list wrapping repeat) smoothly.
  • Stateless Shuffle Controller: Integrates shuffle mode using pointer indices, allowing quick toggle actions without corrupting the base DLL hierarchy.
  • O(1) Track Movement API: Re-links pointers between target and destination nodes instantly to handle drag-and-drop operations efficiently.

Explicit Boundaries (Out-of-Scope)

  • Concrete Core Audio Decoders: Omits real-time audio playback engines, network stream buffers, and codecs.
  • GUI Elements & Waveform Graphics: Simulates active track indices instead of writing browser rendering code, player control interfaces, or volume sliders.

Class Diagram & Entity Relationships

Structural layout showing relationships between the Map indexer, DLL nodes, and playback pointers:

Loading...
  • PlaylistNode: Retains strong references to adjacent nodes (prev and next) alongside Song entity objects.
  • PlaylistManager: Manages DLL head/tail anchors, active node pointers, and lookup tables.

Design Patterns & SOLID Principles

  • State Pattern / State Machine: Manages the active playlist navigation flow using explicit transition rules driven by active shuffle and repeat statuses.
  • Single Responsibility Principle (SRP): Decouples track entity models (Song) from list navigation rules and playlist layout tables (PlaylistManager).
  • Command Pattern (Extension Ready): Simplifies undo/redo tracking for playlist adjustments (e.g. undoing a song deletion or movement) by encapsulating actions.

Core Execution Workflows

O(1) Re-linking Pointers & Shuffle Indices

  1. O(1) Drag-and-Drop Node Movement:
    1. Query the Map index to retrieve references for the source node to move and the target destination node.
    2. Safely detach the source node from its current DLL position by connecting its neighbors directly. Update head/tail anchors if needed.
    3. Connect the source node's next pointer to the target node, and its prev pointer to the target node's prev node.
    4. Update target's neighbors to point to the inserted node, completing the constant-time operation.
  2. Nondestructive Shuffle Navigation:
    1. Fisher-Yates shuffle the song ID collection to build a random playlist order sequence.
    2. Identify the index of the currently playing track in the shuffled sequence, setting it as the starting shuffle index.
    3. Follow the shuffled sequence using the index pointer. When shuffling is disabled, resume standard DLL traversal from the active track.

Concurrency & Thread Safety Strategy

Ensuring correct state transitions during parallel updates and skips:

  • Granular Synchronization & Locks: Thread-safety is achieved by locking operations using ReentrantLocks in Java, Mutex queuing in TypeScript, and RLocks in Python that change adjacent pointers (e.g. deletions or movements) to prevent playlist corruption.
  • Thread-Safe Playback State Updates: Volatile pointer markers prevent race conditions between active background audio players and user-driven playlist modifications.

Complete Clean Code Blueprint

Production reference implementations demonstrating doubly linked lists, O(1) removals, re-linking, and playlist traversal states in Java and Python:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;

class Song {
    private final String id;
    private final String title;
    private final String artist;
    private final int durationSeconds;

    public Song(String id, String title, String artist, int durationSeconds) {
        this.id = id;
        this.title = title;
        this.artist = artist;
        this.durationSeconds = durationSeconds;
    }

    public String getId() { return id; }
    public String getTitle() { return title; }
    public String getArtist() { return artist; }
    public int getDurationSeconds() { return durationSeconds; }

    @Override
    public String toString() {
        return title + " - " + artist;
    }
}

class PlaylistNode {
    Song song;
    PlaylistNode prev;
    PlaylistNode next;

    public PlaylistNode(Song song) {
        this.song = song;
    }
}

enum RepeatMode {
    NONE, ONE, ALL
}

enum ShuffleMode {
    OFF, SHUFFLE
}

class PlaylistManager {
    private PlaylistNode head;
    private PlaylistNode tail;
    private PlaylistNode current;
    private final Map<String, PlaylistNode> nodeMap = new HashMap<>();
    private final ReentrantLock lock = new ReentrantLock();

    private RepeatMode repeatMode = RepeatMode.NONE;
    private ShuffleMode shuffleMode = ShuffleMode.OFF;
    private final List<String> shuffleOrder = new ArrayList<>();
    private int shuffleIndex = -1;

    // O(1) Add to end
    public void addSong(Song song) {
        lock.lock();
        try {
            if (nodeMap.containsKey(song.getId())) {
                throw new IllegalArgumentException("Song already exists in playlist.");
            }

            PlaylistNode newNode = new PlaylistNode(song);
            nodeMap.put(song.getId(), newNode);

            if (head == null) {
                head = newNode;
                tail = newNode;
                current = newNode; // Set pointer if first song
            } else {
                tail.next = newNode;
                newNode.prev = tail;
                tail = newNode;
            }

            if (shuffleMode == ShuffleMode.SHUFFLE) {
                shuffleOrder.add(song.getId());
            }
        } finally {
            lock.unlock();
        }
    }

    // O(1) Delete node
    public void removeSong(String songId) {
        lock.lock();
        try {
            PlaylistNode node = nodeMap.get(songId);
            if (node == null) return;

            // If removing the currently playing node, advance current pointer
            if (node == current) {
                current = getNextNodeLogical();
            }

            // Unlink node
            if (node.prev != null) {
                node.prev.next = node.next;
            } else {
                head = node.next; // Head removed
            }

            if (node.next != null) {
                node.next.prev = node.prev;
            } else {
                tail = node.prev; // Tail removed
            }

            nodeMap.remove(songId);
            shuffleOrder.remove(songId);

            // Adjust shuffle index
            if (shuffleMode == ShuffleMode.SHUFFLE) {
                if (shuffleOrder.isEmpty()) {
                    shuffleIndex = -1;
                    current = null;
                } else {
                    shuffleIndex = Math.min(shuffleIndex, shuffleOrder.size() - 1);
                }
            }
        } finally {
            lock.unlock();
        }
    }

    // O(1) Move node to a different position
    public void moveBefore(String songIdToMove, String targetSongId) {
        lock.lock();
        try {
            if (songIdToMove.equals(targetSongId)) return;

            PlaylistNode toMove = nodeMap.get(songIdToMove);
            PlaylistNode target = nodeMap.get(targetSongId);

            if (toMove == null || target == null) {
                throw new IllegalArgumentException("Song node parameters not found.");
            }

            // 1. Unlink toMove from its current position
            if (toMove.prev != null) {
                toMove.prev.next = toMove.next;
            } else {
                head = toMove.next;
            }

            if (toMove.next != null) {
                toMove.next.prev = toMove.prev;
            } else {
                tail = toMove.prev;
            }

            // 2. Link toMove before target
            PlaylistNode prevOfTarget = target.prev;

            toMove.next = target;
            target.prev = toMove;
            toMove.prev = prevOfTarget;

            if (prevOfTarget != null) {
                prevOfTarget.next = toMove;
            } else {
                head = toMove; // target was head
            }
        } finally {
            lock.unlock();
        }
    }

    public Song getCurrentlyPlaying() {
        lock.lock();
        try {
            return (current != null) ? current.song : null;
        } finally {
            lock.unlock();
        }
    }

    // Next track based on Repeat and Shuffle states
    public Song next() {
        lock.lock();
        try {
            if (current == null) return null;

            if (repeatMode == RepeatMode.ONE) {
                return current.song; // Repeat one: stay on current
            }

            PlaylistNode nextNode = getNextNodeLogical();
            if (nextNode != null) {
                current = nextNode;
                return current.song;
            }
            
            current = null; // Playlist ended
            return null;
        } finally {
            lock.unlock();
        }
    }

    // Previous track
    public Song prev() {
        lock.lock();
        try {
            if (current == null) return null;

            if (repeatMode == RepeatMode.ONE) {
                return current.song;
            }

            PlaylistNode prevNode = getPrevNodeLogical();
            if (prevNode != null) {
                current = prevNode;
                return current.song;
            }

            current = null;
            return null;
        } finally {
            lock.unlock();
        }
    }

    public void setRepeatMode(RepeatMode mode) {
        lock.lock();
        try {
            this.repeatMode = mode;
        } finally {
            lock.unlock();
        }
    }

    public void toggleShuffle() {
        lock.lock();
        try {
            if (shuffleMode == ShuffleMode.OFF) {
                shuffleMode = ShuffleMode.SHUFFLE;
                generateShuffleOrder();
            } else {
                shuffleMode = ShuffleMode.OFF;
                shuffleOrder.clear();
                shuffleIndex = -1;
            }
        } finally {
            lock.unlock();
        }
    }

    private PlaylistNode getNextNodeLogical() {
        if (shuffleMode == ShuffleMode.SHUFFLE) {
            if (shuffleOrder.isEmpty()) return null;
            
            shuffleIndex++;
            if (shuffleIndex >= shuffleOrder.size()) {
                if (repeatMode == RepeatMode.ALL) {
                    shuffleIndex = 0; // Wrap around permutation
                } else {
                    return null; // Stop
                }
            }
            return nodeMap.get(shuffleOrder.get(shuffleIndex));
        } else {
            if (current.next != null) {
                return current.next;
            } else if (repeatMode == RepeatMode.ALL) {
                return head; // Wrap around DLL
            }
            return null;
        }
    }

    private PlaylistNode getPrevNodeLogical() {
        if (shuffleMode == ShuffleMode.SHUFFLE) {
            if (shuffleOrder.isEmpty()) return null;

            shuffleIndex--;
            if (shuffleIndex < 0) {
                if (repeatMode == RepeatMode.ALL) {
                    shuffleIndex = shuffleOrder.size() - 1;
                } else {
                    return null;
                }
            }
            return nodeMap.get(shuffleOrder.get(shuffleIndex));
        } else {
            if (current.prev != null) {
                return current.prev;
            } else if (repeatMode == RepeatMode.ALL) {
                return tail;
            }
            return null;
        }
    }

    private void generateShuffleOrder() {
        shuffleOrder.clear();
        shuffleOrder.addAll(nodeMap.keySet());
        Collections.shuffle(shuffleOrder);

        if (current != null) {
            shuffleIndex = shuffleOrder.indexOf(current.song.getId());
        } else {
            shuffleIndex = 0;
        }
    }

    public List<Song> getOrderedPlaylist() {
        lock.lock();
        try {
            List<Song> songs = new ArrayList<>();
            PlaylistNode runner = head;
            while (runner != null) {
                songs.add(runner.song);
                runner = runner.next;
            }
            return songs;
        } finally {
            lock.unlock();
        }
    }
}

public class Main {
    public static void main(String[] args) {
        System.out.println("=== JAVA MUSIC PLAYLIST DEMO ===");
        PlaylistManager manager = new PlaylistManager();

        Song s1 = new Song("1", "Bohemian Rhapsody", "Queen", 354);
        Song s2 = new Song("2", "Hotel California", "Eagles", 390);
        Song s3 = new Song("3", "Stairway to Heaven", "Led Zeppelin", 482);
        Song s4 = new Song("4", "Imagine", "John Lennon", 183);

        manager.addSong(s1);
        manager.addSong(s2);
        manager.addSong(s3);
        manager.addSong(s4);

        System.out.println("Playlist Order: " + manager.getOrderedPlaylist());
        System.out.println("Currently playing: " + manager.getCurrentlyPlaying());

        System.out.println("Skip next: " + manager.next());
        System.out.println("Skip next: " + manager.next());

        System.out.println("Moving Imagine (s4) before Bohemian Rhapsody (s1)...");
        manager.moveBefore("4", "1");
        System.out.println("New Playlist Order: " + manager.getOrderedPlaylist());

        System.out.println("Setting Repeat ALL and skipping next...");
        manager.setRepeatMode(RepeatMode.ALL);
        System.out.println("Skip next: " + manager.next());
        System.out.println("Skip next: " + manager.next());

        System.out.println("Toggling Shuffle...");
        manager.toggleShuffle();
        System.out.println("Skip next in shuffle: " + manager.next());

        System.out.println("Removing Stairway to Heaven (s3)...");
        manager.removeSong("3");
        System.out.println("New Playlist Order: " + manager.getOrderedPlaylist());
        System.out.println("=== END OF DEMO ===");
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...