The system design question
Design a music streaming service like Spotify. Users can search songs, artists and albums, make playlists and like songs. Playback should start almost immediately for millions of listeners at once, and the service should basically never be down.
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Playing songs | Recommendations |
| Searching songs, artists and albums | Paying artists |
| Playlists and liked songs | Podcasts and video |
| Staying up when things break | Uploading music from labels |
Then agree on what "good" means:
- Starts fast: a song starts playing within about 200 ms of pressing play.
- Always up: playback keeps working even if a database or a whole region is having a bad day.
- Search is quick: results show up as you type, in under about 100 ms.
- Your stuff is safe: a playlist edit or a like is never lost once you've seen it saved.
Rough numbers (5 minutes)
These are assumptions: 100 million listeners a day, each listening about an hour, songs about 3.5 minutes long, 100 million songs in the catalog, and each song stored at three quality levels, about 15 MB in all.
- ~1.5 PB
- of audio files
- ~4M
- people listening at once, on average
- ~650 Gbps
- of audio going out, on average
- ~20K
- songs starting per second
| Question | Rough math | What it means |
|---|---|---|
| How much audio? | 100 million songs × about 15 MB | About 1.5 PB. It goes in object storage, never in a database |
| How many listening at once? | 100 million × 1 hour ÷ 24 hours | About 4 million, maybe 10 million at the evening peak |
| How much bandwidth? | 4 million × 160 kbps | About 650 Gbps on average, well over 1 Tbps at peak. Only a CDN can handle that |
| How many song starts? | 100 million × ~17 songs an hour ÷ 86,400 | About 20K a second. Each one is a small request to us |
The overall design (10 minutes)
- Every song is stored once in object storage, at a few quality levels, cut into small chunks of a few seconds each.
- When you press play, the app asks the playback service for the song. It checks you're allowed to play it and returns short-lived links to the chunks on a CDN.
- The app fetches chunks from the nearest CDN server. Popular songs are already sitting there, so most plays never reach our storage.
- Search goes to a search index built from the catalog. Playlists and likes go to their own databases, split by user or playlist.
- Every play is sent to an event stream, which feeds play counts, charts and anything else that can happen later.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| Search | App | What you've typed so far, plus a filter like songs, artists or albums |
| Get a song, album or artist | App | Its ID. Returns names, cover art links and the track list |
| Start playback | App | Song ID and your connection quality. Returns links to the audio chunks |
| Create or edit a playlist | App | Playlist ID, the change, and a request ID so retries don't apply twice |
| Like or unlike a song | App | Song ID |
| Get my library | App | A cursor and how many items |
| Report plays | App | A batch of song IDs and how long each was played |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Audio chunks | Object storage behind a CDN | Huge, never changes, read from everywhere |
| Songs, albums and artists | Relational database, read through a cache | Structured and linked, and it changes slowly |
| Search index | Search engine, copied to every region | Built from the catalog for matching as you type |
| Playlists | Key-value database, split by playlist ID | A playlist is read and written as one thing |
| Likes and library | Key-value database, split by user ID | All of one person's library sits together |
| Play events | Event stream, then cheap long-term storage | Counts and charts are worked out later |
Step 1: Starting playback almost instantly (12 minutes)
Our servers should never send audio. The playback service only answers 'can this person play this song, and where are the chunks?', which is a small, fast lookup. The bytes come from a CDN, which is just a big cache spread around the world.
Songs are cut into chunks of a few seconds, at a few quality levels. To start fast, the app asks for the first chunk at a lower quality, starts playing as soon as it lands, then switches up once it knows how fast the connection is. You hear sound after one small download, not a whole file.
Popularity is very lopsided. A small set of songs gets most of the plays, so those are almost always already on the CDN server near you. Old or rare songs miss the CDN and come from storage, which is slower. A middle layer of cache between the CDN and storage stops those misses from all hitting storage at once.
The app can also hide the wait. While a song plays, it fetches the start of the next song in the playlist or album, so skipping feels instant. It keeps recently played songs on the phone too.
Keeping links private
The chunk links are signed and expire after a few minutes. Anyone can fetch from the CDN, but only with a link we handed out. That stops people sharing a permanent link to a song.
Step 2: Search for songs, artists and albums (8 minutes)
We don't search the catalog database. It's built for 'get song 123', not 'everything that starts with "tay"'. So we keep a separate search index built from the catalog, with entries for song, artist and album names.
The catalog only changes when new music is added, which is mostly a weekly batch from labels. Each change goes on a queue and indexers update the index from it. Being a few minutes behind is fine.
Search runs as you type, so matching has to work on the start of words and forgive small typos. Ranking matters more than matching: lots of things match 'love', so sort by how often each result gets played, which we already count from play events.
The index for 100 million songs is a few hundred GB, so it's split into shards, with copies in every region. Popular searches repeat a lot, so cache the results of the most common ones for a minute or so.
Step 3: Playlists, likes and staying up (10 minutes)
Likes are a long list per person, read all at once when you open your library. A key-value database split by user ID fits that well, and it's the usual relational vs non-relational call. Splitting by user is plain sharding, and everyone's library is roughly the same size.
Playlists are split by playlist ID, since a popular playlist is read by millions of people who aren't its owner. Those hot ones get cached. Every edit carries a request ID, so if the app retries after a timeout, the same song isn't added twice. That's idempotency.
Shared playlists can get two edits at once. Give each playlist a version number, and reject an edit made against an old version. The app reloads and tries again. Edits are rare, so that almost never happens.
What if a whole region goes down?
Run in several regions. The catalog and search index barely change, so every region has a full copy. Playlists and likes are copied between regions within a second or so. If a region fails, its users move to the next nearest one, and at worst a like from the last second shows up late. Choosing to stay up over being perfectly in sync is a consistency trade we're happy to make here.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| Audio from a CDN, our servers only hand out links | Streaming audio through our own servers | We'd need over a Tbps of our own bandwidth | Never at this scale |
| Small chunks at a few quality levels | One file per song | Playback starts after one small download and adjusts to the connection | Songs were tiny and connections always fast |
| Separate search index | Searching the catalog database | Matching as you type needs a different kind of index | The catalog was small enough for a database's built-in search |
| Playlists and likes copied between regions a second late | One main region for all writes | Any region can take writes, so we stay up when one fails | Edits needed to be exact everywhere, like a bank balance |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| A CDN has trouble in one area | The app switches to a second CDN for that area |
| Object storage is slow | Popular songs still play from the CDN. Only rare songs are affected |
| The playlist database is down | Show the copy the app saved last time, read-only, and queue edits until it's back |
| The search index is down | Search fails, but playback and libraries still work. Show recent searches from the app |
| A huge album drops at midnight | Push its first chunks to the CDN ahead of time, so the rush hits a warm cache |
Follow-up questions (5 minutes)
How would you support offline listening?
Let the app download the chunks for a playlist and store them encrypted, with a license that has to be renewed every few weeks.
How do you count plays for charts and paying artists?
Read the play events from the stream, drop duplicates by event ID, and add them up in batches. Nobody needs it to the second.
How would 'listening with friends' work?
Keep a live connection per listener and have one session hold the current song and position. Each app plays from the CDN on its own and syncs to that position.
How the 60 minutes add up
| Minutes | What you're doing |
|---|---|
| 0–5 | Agree on what you're building |
| 5–10 | Rough numbers |
| 10–20 | Draw the overall design |
| 20–32 | Starting playback almost instantly |
| 32–40 | Search for songs, artists and albums |
| 40–50 | Playlists, likes and staying up |
| 50–55 | What could go wrong |
| 55–60 | Follow-up questions |
Key concepts used here
- Caching strategiesMake reads faster, and figure out what to do when data changes.
- Message queues and event streamsKeep services from depending on each other, and handle traffic spikes.
- Relational vs non-relational databasesPick a database based on how you'll actually use it.
- Sharding and consistent hashingSplit data across machines and keep it balanced.
- Idempotency and retriesMake sure a retried request doesn't happen twice.
- Consistency and the CAP theoremWhat happens when copies of your data can't agree.
- Search and inverted indexesFind text fast, rank what comes back, and keep the index fresh.
- Blob storage and CDNsStore big files cheaply and serve them from close to your users.
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Amazon.