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-S3to 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
- User Data -
- Daily Bandwidth
- avg duration of song is
3 Min -> 180sec - avg bitrate 180Kbps
- about
180*180Kbit -> ~4MBper 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 -> 18GBdata- total cost depends on service provider bandwidth tarrif
- avg duration of song is
High Level Architecture
High Level Sequence Diagram
Data Models
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
JWTunless 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
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.
