FDEInterviews logoFDE/Interviews
ML System Design (Product) / 02
hard★ EssentialSpotifyAppleAmazon

Design a music recommendation system (Spotify-style).

A track lasts three minutes, a session lasts an hour, and a new song has zero plays the day it drops. The interview is about blending collaborative filtering with audio content embeddings, surviving cold start on both sides, and reading a skip as the loud negative it is.

Updated Aug 2026 · Grounded in real Forward Deployed Engineer interview loops and written to a senior-engineer editorial bar.

TL;DR: Blend collaborative filtering (the play matrix factorized or learned into user/track embeddings) with content embeddings derived from audio and metadata, so new tracks and new users are not invisible. Retrieve with ANN over those embeddings, re-rank with session/sequence context, treat early skips as strong negatives, and run a contextual bandit to explore so the catalog does not collapse to the top 1%.

How to approach it

Start with what makes music different from a generic feed: items are consumed in sequence within a session, the same track is replayed (so repeats are good, not staleness), feedback is almost entirely implicit, and a skip in the first five seconds is a loud negative while a full listen is a quiet positive. Then name the binding problem, cold start on both sides, because pure collaborative filtering fails the day a user signs up or an artist uploads. Ask whether the surface is a curated playlist, radio/autoplay, or home-page shelves, because that changes how much sequence and exploration matter. Cover the embedding blend, labels, session modeling, cold start, and exploration.

A strong answer

The core is a two-signal embedding space. Collaborative filtering captures taste correlation: factorize the user-by-track play matrix (weighted ALS on implicit counts, or learn it with a two-tower model) so that tracks co-listened by similar users land near each other, even when they share nothing acoustically. That alone is the strongest signal for established users and tracks. But it is blind to anything with no plays, so you add content embeddings: a model over the raw audio (a CNN or transformer on spectrograms producing a vector for tempo, timbre, energy, vocal/instrumental) plus metadata (artist, genre, release, language) and, where available, text from reviews and editorial tags. The two are combined, either concatenated into one ranker or kept as separate retrieval sources that a re-ranker merges.

rendering diagram…

Labels are implicit and need care. Rank the signals: a save or add-to-playlist is the strongest positive, a full play (or >30s) is a solid positive, a repeat is positive (do not penalize it as we would in a content feed), an early skip (<5 to 10s) is a strong negative, and a never-served track is a non-label, not a negative. You typically weight examples by listen fraction so a 90% listen counts more than a 35% one. As with any recommender, un-served items are sampled as easy negatives for retrieval, never treated as confirmed dislikes.

Session and sequence context is where music diverges from feed ranking. A good queue depends on what just played: you do not jump from a focus-piano track into thrash metal. So the re-ranker takes the last N tracks of the session, time of day, and device as features, and sequential models (a transformer or GRU over the session) predict the next track conditioned on the trajectory, not just static user taste. This is what makes radio and autoplay feel coherent.

Cold start is the question they are really asking, and it is two-sided:

Cold-start caseWhy CF failsWhat you actually do
New user (no plays)No row in the play matrixOnboarding picks (seed artists), demographic/context priors, popularity, then personalize fast over the first sessions
New track (no plays)No column in the play matrixContent/audio embedding places it near acoustically similar tracks; small exploration budget to earn first plays
New artistNo catalog signal at allContent embeddings plus editorial/genre tags; bandit gives a guaranteed exploration slice so they can ever get heard

The new-artist case is the honest test. With CF only, a song with zero plays is invisible forever, a self-fulfilling rich-get-richer loop. Content embeddings break it by placing the track from its audio alone, and an explicit exploration budget guarantees it impressions. Frame exploration as a contextual bandit (Thompson sampling or epsilon-greedy over candidates): you spend a small fraction of slots on uncertain items to learn their true reward instead of always exploiting the current best. Without it the system collapses to the head of the catalog and never discovers that a new track is great.

Measurement mirrors the feed case but with music-specific proxies. Offline: recall@k and NDCG against held-out plays, plus a calibration check on predicted play probability. Online: A/B on the metrics that signal satisfaction rather than raw plays, skip rate (lower is better), session length, save rate, and longer-horizon retention and discovery (are users hearing artists they had not before). Watch catalog coverage and a diversity metric as guardrails, because a model can lift short-term plays by serving only sure things and slowly starving the catalog.

The close: the recommender is not one model, it is CF for what you know, content embeddings for what you do not, sequence context for the session, and a bandit so the system keeps learning instead of feeding back on itself.

What interviewers probe next

  • "A brand-new artist uploads today, how do they ever get a play?" Content/audio embeddings place the track without any play history, and a guaranteed exploration slice (the bandit) hands it impressions so it can earn real reward. Pure CF would leave it invisible forever.
  • "How do you keep recommendations fresh without whiplash?" Sequence-aware re-ranking keeps the queue coherent, repeats are allowed (music is not news), and a diversity/novelty term plus exploration prevents the same 20 tracks looping.
  • "Implicit feedback only, how do you trust a skip?" An early skip (<5 to 10s) is a strong negative; a late skip is weak; a full or repeat listen is positive. Weight by listen fraction and do not treat un-served tracks as negatives.

Common mistakes

Proposing collaborative filtering as the whole answer and then having no story for the day-one user or the zero-play track. Treating a repeat listen as staleness to suppress, which is right for a news feed and wrong for music. Ignoring session order so the queue lurches between moods. Counting every non-play as a negative, which buries the long tail. And forgetting exploration entirely, so the system exploits the popular head, never collects feedback on new items, and decays into a rich-get-richer loop that an interviewer will name immediately.

Key takeaways

  • Blend CF embeddings (for known users/tracks) with content/audio embeddings (for cold items) and retrieve both via ANN.
  • Cold start is two-sided: content embeddings place new tracks, onboarding and priors place new users, and a bandit gets new artists heard.
  • Read implicit feedback by strength: save > full play > repeat > weak; early skip is a strong negative, un-served is not a negative.
  • Model session sequence for queue coherence and decide online on skip rate, saves, retention, and discovery, not raw play count.
HOW DID IT GO?
0
UP NEXT ON YOUR JOURNEY
FEDITOR'S NOTE

The signal here is handling the two-sided cold-start honestly: collaborative filtering alone cannot place a user with no history or a track with no plays, so strong candidates reach for content/audio embeddings and a contextual bandit for exploration rather than hand-waving 'we'd use popularity.' They also treat a skip in the first few seconds as a strong negative and design session/sequence context in. Probe with 'a brand-new artist uploads today, how do they ever get a play?' to separate people who have built this from people who have read about matrix factorization.

DISCUSSION · 0

No comments yet — be the first to share your approach.