Spotify Frontend Interview
Composite Spotify-style frontend prep: player state, lists at scale, and JS depth. Media loops vary by team, level, and year.
- 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.
timeupdateis coarse. Smooth scrubbing needsrequestAnimationFramewhile 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
isPlayinginonClickinstead of fromplaying/pause - Ignoring the
play()promise (autoplay policy) timeupdate→setState→ full playlist rerenderscrollIntoViewon 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)
- Weeks 1-2: Event loop, promises, rAF vs timers. Teach them back. One JS util a day.
- 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.
- Week 5: Fifteen-minute design outline of home + playlist + now playing. Keyboard, empty states, list perf budget.
- 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.”