System design is the first step in the elegant process of software development. So, lets design a system like Spotify.

Functional Requirements

  • User can sign up and login to the system.
  • User can search for a song, artist, album, playlist, etc.
  • User can create a playlist and add songs to it.
  • User can share a playlist with other users.
  • User can listen to a song.
  • User can download a song.

Non-Functional Requirements

  • About 1M users
  • 50M songs available in the system
    • sometimes, people reupload contents so consider it to be 70M songs
  • Higher Availability (99.99% uptime)
  • Support for Ogg & AAC audio formats with adaptive bitrate streaming
    • 64 Kbps for mobile data saving
    • 128 Kbps for standard quality
    • 320 Kbps for premium users
  • We will be using blob storage like AWS-S3 to store songs

System Capacity Calculations

  • Storage requirements for 1M songs
    • 70M x 5MB(avg filesize) = 350TB of storage
    • 350 TB x No. of Replicas across different locations
  • Database size
    • User Data - 1M * ~1KB = 1GB
    • Song info/metadata - 70M * 1KB = 7GB
    • Playlist info - 100K * 0.5KB = 50MB
    • Total: ~ 9GB
  • Daily Bandwidth
    • avg duration of song is 3 Min -> 180sec
    • avg bitrate 180Kbps
    • about 180*180Kbit -> ~4MB per stream
    • lets assume avg user listens to 15 songs daily & daily active user-count is 300K out of 1M
    • 300K(0.3M People) * 15 * 4MB -> 18GB data
    • total cost depends on service provider bandwidth tarrif

High Level Architecture

High Level Architecture Of Spotify

High Level Sequence Diagram

Overview Sequence Diagram Of Spotify

Data Models

Overview Sequence Diagram Of Spotify

API Design

Now we are going to design API end-points with which our front-end app or mobile app will interact. Firstly all of our resources/models like User, Song, Playlist and Album

Search Feature

Search Songs

1
GET /songs/search?q={query}&limit={pagesize}&page={page}&genre={genre}

Response

1
[{ "id": 12, "title": "Shape of You", "duration_ms": 235000, "artist": "Ed Sheeran" }]

Search Albums

1
GET /albums/search?q={query}&limit={pagesize}&page={page}

Response

1
[{ "id": 1, "title": "Divide", "artist_id": 3 }]

Search Playlists

1
GET /playlists/search?q={query}&limit={pagesize}&page={page}

Response

1
[{ "id": 8, "title": "Morning Mix", "song_count": 25 }]

Search Artists

1
GET /artists/search?q={query}&limit={pagesize}&page={page}

Response

1
[{ "id": 1, "name": "Taylor Swift", "country": "US" }]

Authentication

We will use 2 Token Authetication Apporach for performance and security. We will use JWT for sharing Access Token to the FrontEnd/Mobile App.

Access Token

We will store the private key used to sign the JWT for Access Token in Rails’s credentials file. It will contain claims like exp and iat for simplicity. See This for more valid globally accepted claims. Token’s validity will be for just 5 minutes for security.

Format

Header

1
2
3
4
{
  "alg": "HS256",
  "typ": "JWT"
}

Payload

1
2
3
4
{
  "exp": 1516239322,
  "iat": 1516239022
}

Secret

Secret will be stored in Rails’s credentials file.

1
a-string-secret-at-least-256-bits-long

JWT

1
2
3
4
5
# Structure
{base64encoded_header}.{base64encoded_payload}.{digest/signature}

# Example
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30

Do note that for signed tokens, this information, though protected against tampering, is readable by anyone. Do not put secret information in the payload or header elements of a JWT unless it is encrypted.

API
1
2
3
4
5
6
7
8
9
# POST /auth/login
# Authenticate and return access token.
# request params
{
  "email": "user@example.com",
  "password": "strongpassword123",
  "device_info": "iPhone 15 Pro - iOS 18.1",
  "ip_address": "2405:9800:bc00:1234::1"
}

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "user": {
    "id": 42,
    "name": "Durent Timre",
    "email": "user@example.com",
    "account_type": "premium",
    "country": "NP",
    "last_login": "2025-11-08T06:23:41Z"
  },
  "tokens": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "d9e1caae-939f-4c1b-9c1a-1286f3c5bb54",
    "token_type": "Bearer",
    "expires_in": 900
  },
  "meta": {
    "device_info": "iPhone 15 Pro - iOS 18.1",
    "ip_address": "2405:9800:bc00:1234::1",
    "issued_at": "2025-11-08T06:23:41Z"
  }
}
Authentication in Backend

Here is a basic backend implementation of token authentication

1
2
3
4
5
6
7
8
9
def authenticate_user!
  auth_header = request.headers['Authorization']
  token = auth_header.split(' ').last if auth_header
  decoded = JWT.decode(token, Rails.application.credentials.secret_key_base, true, algorithm: 'HS256')
  payload = decoded.first
  @current_user = User.find(payload['user_id'])
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
  render json: { error: 'Unauthorized' }, status: :unauthorized
end

Refresh Token

This is not a JWT rather a randomly generated long string. It will be stored in refresh_tokens table uniquely.

Token Generate Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def generate_refresh_token(user, device_info:, ip_address:)
  raw_token = SecureRandom.hex(64)
  hashed_token = Digest::SHA256.hexdigest(raw_token)

  RefreshToken.create!(
    user_id: user.id,
    token_hash: hashed_token,
    expires_at: 30.days.from_now,
    device_info: device_info,
    ip_address: ip_address
  )

  raw_token # return to client
end
API
1
2
3
# POST /auth/refresh
# request params
{ "refresh_token": "<old_refresh_token>" }

Response

1
2
3
4
5
6
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI...",
  "refresh_token": "e24db96e0a2c92c7f8a7a1a099a1f0a9a5b6b8a...",
  "expires_in": 900,
  "token_type": "Bearer"
}

Authentication Sequence Diagram

sequence diagram of 2 token authentication system

Songs API

GET /songs/:id

Response

1
2
3
4
5
6
7
8
{
  "id": 12,
  "title": "Shape of You",
  "artist": { "id": 2, "name": "Ed Sheeran" },
  "album": { "id": 7, "title": "Divide" },
  "genres": [{ "id": 3, "name": "Pop" }],
  "duration_ms": 235000
}

If you want to play the audio, you will need to make another API call /songs/:id/stream. This will return signed Stream URL which you can play.

POST /songs/

Important Headers:

1
2
Content-Type: multipart/form-data
Authorization: Bearer <JWT>

request params

1
2
3
4
5
6
7
8
{
  "title": "New Song",
  "artist_id": 1,
  "album_id": 3,
  "duration_ms": 245000,
  "genre_ids": [1, 3],
  "file": "FileObject (audio/mp3, wav, flac)"
}

response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "id": 94,
  "title": "New Song",
  "artist": {...},
  "album": {...}},
  "genres": [{...},{...}],
  "duration_ms": 245000,
  "file": {
    "url": "https://cdn.spotify-clone.com/songs/94-new-song.mp3",
    "mime_type": "audio/mpeg",
    "size": 7024512
  },
  "created_at": "2025-11-08T07:00:43Z"
}

GET /songs/feed?page={page}&limit={page_size}

Generates list of recommended songs based on the user’s preferences, previous searches and history. We might use KNN based model to generate recommendations.

1
2
3
4
5
6
7
8
9
10
11
[
  {
  "id": 12,
  "title": "Shape of You",
  "artist": { "id": 2, "name": "Ed Sheeran" },
  "album": { "id": 7, "title": "Divide" },
  "genres": [{ "id": 3, "name": "Pop" }],
  "duration_ms": 235000
  },
...
]

GET /songs/:id/stream

You can return a temporary URL valid for a few minutes, generated via S3 SDK or JWT-based URL signer.

1
2
3
4
5
6
7
8
9
def generate_stream_url(song)
  signer = Aws::S3::Presigner.new
  signer.presigned_url(
    :get_object,
    bucket: "my-music-bucket",
    key: song.file_key, # e.g. "songs/12.mp3"
    expires_in: 300 # 5 minutes
  )
end
1
2
3
4
5
6
7
8
9
10
{
  "stream_url": "https://cdn.example.com/songs/12.mp3?token=abc123&expires=1731087400"
}

# example

{
  "stream_url": "https://my-music-bucket.s3.amazonaws.com/songs/12.mp3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=..."
}

This prevents unauthorized access and enables private streaming.

Other Actions

DELETE /songs/:id/

Soft delete or remove a song the system.

PATCH /songs/:id

Update metadata for a song.

Upgraded Architecture For Scalability

As the system grows to support 10 million users, with 1 million daily active users and over 200 million songs, the infrastructure must be designed for horizontal scalability and fault tolerance.

To handle such a large-scale workload:

  • Multiple load balancers should be deployed across availability zones to distribute incoming traffic evenly and provide redundancy.
  • A health monitoring system should continuously check load balancer status, automatically replacing or rerouting traffic if one becomes unhealthy.
  • Auto-scaling policies should ensure that additional application and database instances are provisioned automatically based on real-time demand.

upgraded-architecture.svg