ESC

Type to search the knowledge base.

Spotify Frontend Interview

Composite Spotify-style frontend prep: player state, lists at scale, and JS depth. Media loops vary by team, level, and year.

advanced6 min read
  • interview-experience
  • spotify

Composite guidance based on publicly shared candidate reports and common hiring patterns for frontend / UI roles on media and player products. Processes vary by role, level, team, and year. Always confirm with your recruiter. Not a personal diary, and not affiliated with Spotify.

A Spotify frontend interview is easy to misread as “make a play button look right.” The miss is usually two sources of truth. The click sets isPlaying = true and also calls audio.play(). A pause event from a route change then fights the click, and the seek bar jumps. Player UI is a state machine mirrored from the media element. The second miss is a playlist that maps ten thousand tracks into the DOM.

Media loops score playback, lists, and keyboard. Close cousin: Netflix. Spotify-shaped work leans on continuous playback, queue vs playlist, and media APIs.

Key takeaways

  • Drive UI from media events. Do not set “playing” in the click handler and hope the element agrees.
  • Playlists virtualize. Jump-to-current is offset math. The current row may not be mounted.
  • Keyboard and Media Session must share one controller with the on-screen chrome.
  • timeupdate is coarse. Smooth scrubbing needs requestAnimationFrame while playing, not a render on every event.
  • Confirm team (web player, ads, tools, platform) and whether the loop is coding-heavy or design-heavy.

Player state, virtualized lists, and web-platform depth

Stage Focus Format
Recruiter Role, team, stack, level Call
JS / problem-solving Language, async, small utils or DSA Shared editor or OA
Machine coding Player chrome, list, search 45-90 min
Product / web Playback UX, perf, a11y, platform APIs Conversation + code
Hiring manager Ownership, tradeoffs Call

Some teams insert a take-home. Some run two coding rounds and a design talk (home, playlist, now playing). Do not memorize a four-round script. Ask. They score correct JS, a working play/pause/next/seek path, and respect for the platform: media element, keyboard, lists that stay scrollable.

Playback as a state machine

Treat HTMLMediaElement as canonical. Your store mirrors it. Clicks call play(), pause(), or set currentTime. Reducers react to playing, pause, waiting, ended, error, loadedmetadata.

const PLAYBACK = {
  idle: { load: 'loading' },
  loading: { canplay: 'paused', error: 'error', abort: 'idle' },
  paused: { playing: 'playing', load: 'loading' },
  playing: { pause: 'paused', waiting: 'buffering', ended: 'ended', error: 'error' },
  buffering: { playing: 'playing', pause: 'paused', error: 'error' },
  ended: { playing: 'playing', load: 'loading' },
  error: { load: 'loading' },
};

function reducePlayback(status, event) {
  return PLAYBACK[status]?.[event] ?? status;
}

Illegal transitions stay put. play while idle with no src is a no-op you explain. seek before duration is finite is a no-op. audio.play() returns a promise. Autoplay policy can reject it. Catch that and stay paused. Do not flip the button to Playing on click.

ended is product policy. Repeat off: stop, or auto-advance. Repeat one: seek 0 and play(). Repeat all: next, wrapping. Shuffle is a permutation of track ids, not Math.random() on every Next. Queue is a separate list from the playlist you are browsing. Play-from-search should not silently destroy a queue the user built unless the product says so. Say the rule.

Practice chrome with video player controls. Design talk: design a video player experience. Complex transitions belong in a reducer (useReducer).

Seek fight: keep a seeking flag while the user drags; ignore element time until seeked. timeupdate is a few times a second. For a smooth bar, sample currentTime inside requestAnimationFrame while playing, and cancel rAF on pause. That is an event loop question: do not schedule heavy list work on every timeupdate.

Virtualized lists and the now-playing row

Liked songs, a podcast catalog, a queue of thousands: mounting every row freezes the main thread. List virtualization renders a window plus overscan. Why it matters: virtual lists and performance.

Key rows with track id, never the index. Spacer height is count * rowHeight (fixed height unless they ask you to measure). “Jump to current” sets scrollTop from the index. Do not querySelector a node that virtualization unmounted. Now-playing is data (trackId === currentId). Do not steal focus when the track advances. Related: infinite scroll feed. Search: debounce.

Keyboard and the web platform

Space toggles play, arrows seek, m mutes, media keys via the Media Session API. Listen at a high level, and bail if the target is a typing field (input, textarea, select, contenteditable).

One controller should back on-screen buttons, keyboard, and Media Session actions (play, pause, previoustrack, nexttrack, seekto). Accessible names on play/pause, a seek slider that exposes value and max, visible focus: keyboard accessibility checklist. Use a real button, not a clickable div.

Stay in frontend scope. DRM and codecs are not yours to invent in a 60-minute round. You do own buffering copy, empty playlist, and artwork width/height so a 3000px cover does not jump a 48px row.

JavaScript depth

Screens still look like general frontend JS: closures, promises, the event loop, array transforms, sometimes a polyfill, sometimes medium DSA. Media work makes the event loop concrete: play() promise, timeupdate cadence, rAF vs timers, aborting artwork fetch when the track changes (AbortController).

Prep from the JavaScript interview guide and output-based questions. If they use DSA in JS: LRU Cache, Two Sum. Restate, example, standalone function, dry-run empty playlist, play() reject, seek before metadata. Naive list renders are long tasks.

Footguns

  • Setting isPlaying in onClick instead of from playing / pause
  • Ignoring the play() promise (autoplay policy)
  • timeupdate → setState → full playlist rerender
  • scrollIntoView on a virtualized row that is not in the DOM
  • Index keys on a shuffleable list; Space stolen from the search box
  • Crossfade as CSS opacity on artwork (that is not audio)

Preparation plan (5-7 weeks)

  1. Weeks 1-2: Event loop, promises, rAF vs timers. Teach them back. One JS util a day.
  2. Weeks 3-4: Timed DSA in JS plus one MC rebuild per weekend: player chrome from video player controls, then a virtualized playlist with a current-track jump.
  3. Week 5: Fifteen-minute design outline of home + playlist + now playing. Keyboard, empty states, list perf budget.
  4. Weeks 6-7: Mocks. After each, write three concrete fixes. Confirm device target (desktop web vs mobile web) with the recruiter.

Out loud: “The media element is source of truth. Clicks call play or pause. UI status comes from events, including a rejected play promise. Repeat and next are policies on ended. Shuffle is a permutation of ids. The queue is not the browse playlist. The list virtualizes with track-id keys. Keyboard and Media Session call the same controller, and Space is ignored when the target is an input.”

Further reading