How to Design a Music Streaming Service
Building a Spotify-Scale Platform — Audio Encoding, Adaptive Bitrate, CDN, Recommendations & More
1. Introduction and Why Music Streaming is Hard
Music streaming is one of the most technically demanding consumer applications on the internet. Every day, platforms like Spotify serve over 100 million hours of audio to hundreds of millions of users across every device imaginable, including smartphones, smart speakers, cars, desktops, wearables, and gaming consoles. The system must deliver studio-quality audio with sub-200ms startup latency, handle massive traffic spikes during new album releases, support hundreds of audio codecs and bitrates, and orchestrate a global content delivery network spanning thousands of edge locations. It must do all of this while respecting complex music licensing agreements, calculating per-stream royalties across dozens of rights holders, preventing piracy through DRM, and continuously updating recommendation models to keep users engaged.
The core technical challenges of a music streaming service break down into several interconnected domains. First, there is the audio pipeline: ingesting tracks from record labels in various formats, encoding them into multiple bitrates and codecs, normalizing loudness, and distributing them globally. Second, there is the streaming infrastructure: serving audio chunks over HTTPS with adaptive bitrate switching so that playback is seamless whether a user is on a fiber connection or a 3G cellular network. Third, there is the content management layer: maintaining a catalog of 100+ million tracks with rich metadata, handling daily updates from distributors, and keeping search indices in sync. Fourth, there is the intelligence layer: recommendation engines that personalize content for each user, editorial curation tools, and social features that drive engagement. Finally, there is the business infrastructure: royalty calculation, licensing compliance, artist analytics, and fraud detection.
Understanding how existing platforms solve these problems provides valuable design intuition. Spotify uses Ogg Vorbis at multiple bitrates with a proprietary CDN and client-side adaptive streaming. Apple Music uses AAC with HLS (HTTP Live Streaming) and leverages Apple's massive CDN infrastructure. Tidal offers lossless FLAC streaming with MQA encoding for audiophiles. Each platform makes different tradeoffs between audio quality, bandwidth efficiency, licensing costs, and device compatibility. Our design will explore the architecture decisions behind these tradeoffs and present a production-grade system that balances all of these concerns.
Real-World Case Studies
| Platform | Codec | Peak Users | Key Innovation |
|---|---|---|---|
| Spotify | Ogg Vorbis (96-320 kbps) | 220M subscribers | Client-side ML for recommendations, Canvas video loops |
| Apple Music | AAC (256 kbps) / ALAC (lossless) | 100M+ subscribers | HLS adaptive streaming, Spatial Audio with Dolby Atmos |
| Tidal | FLAC / MQA | 7M+ subscribers | Lossless HiFi tier, artist direct payouts |
| YouTube Music | AAC / Opus | 100M+ subscribers | Video plus audio unification, user-generated content |
| Amazon Music | AAC / FLAC (Ultra HD) | 82M+ users | Alexa integration, spatial audio, podcast bundling |
Spotify's approach to audio delivery is particularly instructive. Rather than streaming entire tracks, Spotify splits each track into small audio chunks of approximately 1 to 2 seconds each and uses a proprietary client-side player that requests chunks dynamically. The client monitors network conditions and switches between quality tiers in real time. When network bandwidth drops, the player seamlessly switches from a 320 kbps Ogg stream to a 96 kbps stream without any audible interruption. This adaptive bitrate approach is fundamental to every modern streaming platform and forms a core part of our design.
The scale of modern music streaming is staggering. Spotify reported in 2024 that users collectively stream over 100 million hours of audio per day. At an average bitrate of 160 kbps, that translates to roughly 1,728 petabytes of data transferred monthly through their CDN. To put that in perspective, that is approximately 1.7 exabytes, more data than most internet backbone providers handle. Managing this volume requires a purpose-built CDN architecture with aggressive caching strategies, origin shielding, and intelligent request routing. We will explore each of these components in detail throughout this article.
2. Functional and Non-Functional Requirements
Functional Requirements
- Audio Playback: Users can stream music at multiple quality levels with adaptive bitrate switching. Playback must start within 200ms and support gapless transitions between tracks.
- Music Catalog: Browse, search, and explore a catalog of 100M+ tracks organized by artist, album, genre, mood, era, and activity.
- Playlist Management: Create, edit, collaborate on, and share playlists. Support algorithmic playlists such as Discover Weekly and Release Radar, as well as editorial playlists.
- Recommendations: Personalized track suggestions based on listening history, explicit preferences, and collaborative filtering.
- Offline Playback: Download tracks for offline listening with DRM protection. Sync state across devices.
- Social Features: Follow artists and friends, see activity feeds, share tracks, create collaborative playlists, and host social listening sessions.
- Lyrics: Time-synced lyrics displayed during playback with translation support.
- Podcasts: Support for long-form audio content with chapters, speed control, and transcript search.
- Artist Dashboard: Real-time analytics for artists showing streams, demographics, playlist placements, and revenue.
- Audio Fingerprinting: Identify unknown tracks from audio snippets similar to Shazam.
- Live Audio: Support for live radio, live sessions, and real-time audio broadcasting.
Non-Functional Requirements
| Requirement | Target | Rationale |
|---|---|---|
| Playback Startup Latency | Less than 200ms (first audible byte) | Users abandon slow-loading players |
| Availability | 99.99% | 24/7 service, millions in lost revenue per hour of downtime |
| Concurrent Streams | 100M+ simultaneous | Peak events like New Year and album drops concentrate traffic |
| Audio Quality Range | 24 kbps to 1411 kbps (lossless) | Support from low-bandwidth to audiophile tiers |
| Catalog Size | 100M+ tracks, growing 100K/day | Industry-scale content library |
| Search Latency | Less than 100ms (P99) | Responsive search experience |
| CDN Cache Hit Ratio | Greater than 95% | Minimize origin load and bandwidth costs |
| Offline Sync | Background, no user intervention | Frictionless offline experience |
| Loudness Normalization | -14 LUFS (Spotify standard) | Consistent volume across tracks |
| DRM Enforcement | AES-128 or Widevine L1 | Protect rights holder content |
Key Design Tradeoffs
| Tradeoff | Option A | Option B | Our Choice |
|---|---|---|---|
| Codec Selection | AAC (better compatibility, licensing) | Ogg Vorbis (open, no licensing fees) | AAC primary + Opus fallback for WebRTC |
| Streaming Protocol | HLS (Apple ecosystem dominance) | MPEG-DASH (open standard) | HLS primary + DASH for Android/web |
| CDN Strategy | Single CDN provider | Multi-CDN with intelligent routing | Multi-CDN (CloudFront + Fastly + GCP) |
| Recommendation ML | Server-side (centralized models) | Client-side (on-device personalization) | Hybrid: server for discovery, client for personalization |
| DRM System | Widevine (Google) / FairPlay (Apple) | Custom DRM with server-side decryption | Platform-native DRM (Widevine + FairPlay) |
3. Capacity Estimation
User and Stream Volume
- Total registered users: 500 million
- Daily active users: 300 million
- Average listening time per user per day: 30 minutes
- Total daily stream hours: 90 million hours
- Peak concurrent streams (evening hours): 50 million
- Average track duration: 3.5 minutes
- Tracks played per user per day: approximately 8.5
- Total daily track plays: approximately 2.55 billion
Bandwidth and CDN
- Average bitrate: 160 kbps (mid-quality tier)
- Bandwidth per concurrent stream: 20 KB/s
- Peak aggregate bandwidth: 50M times 20 KB/s equals 1 TB/s
- Daily data transfer: 90M hours times 3600s times 20 KB/s equals approximately 6.48 petabytes per day
- Monthly CDN transfer: approximately 194 petabytes
- CDN edge locations needed: 200+ points of presence globally
- Cache hit ratio target: 95%+, so origin serves approximately 5% equals approximately 32 TB per day
Storage
| Data Type | Size per Track | Total (100M tracks) | Growth Rate |
|---|---|---|---|
| Original WAV (24-bit/96kHz) | approximately 250 MB | 25 PB | approximately 25 TB/day |
| AAC 320 kbps | approximately 8.4 MB | 840 TB | approximately 8.4 TB/day |
| AAC 128 kbps | approximately 3.4 MB | 340 TB | approximately 3.4 TB/day |
| AAC 64 kbps | approximately 1.7 MB | 170 TB | approximately 1.7 TB/day |
| Album Art (300px) | approximately 50 KB | 5 TB | approximately 5 GB/day |
| Lyrics (timed XML) | approximately 5 KB | 500 GB | approximately 500 MB/day |
| Metadata (JSON) | approximately 2 KB | 200 GB | approximately 200 MB/day |
Database and Cache
- Track metadata reads: 10 billion per day equals approximately 115K QPS average, approximately 350K QPS peak
- User profile reads: 300M per day equals approximately 3.5K QPS average, approximately 12K QPS peak
- Playback state writes: 300M users times approximately 10 events per minute equals 50M writes per minute equals approximately 833K QPS
- Redis cache size (hot tracks): 1M tracks times 10 KB metadata equals approximately 10 GB
- Redis cache size (user sessions): 50M active sessions times 2 KB equals approximately 100 GB
- Search index size: 100M tracks times 5 KB equals approximately 500 GB (Elasticsearch cluster)
4. High-Level Architecture Overview
The music streaming architecture is organized into six major subsystems: the Client Layer (native apps and web player), the API Gateway (authentication, rate limiting, request routing), the Streaming Service (audio delivery and adaptive bitrate), the Content Platform (catalog, metadata, search, recommendations), the Social and Engagement Layer (playlists, social features, activity), and the Business Platform (royalties, analytics, licensing). These subsystems communicate through a combination of synchronous REST/gRPC APIs for user-facing requests and asynchronous event streams via Kafka for background processing.
Request Flow: Playing a Track
- User taps play: The client app sends a request to the API Gateway with the track ID and current network conditions including bandwidth estimate and connection type.
- Authentication and Authorization: The Gateway validates the JWT token, checks subscription status, and verifies the user has rights to play the track including geo-restrictions and licensing requirements.
- Manifest Generation: The Streaming Service generates an HLS manifest (master playlist) listing all available bitrate variants for the track. The manifest includes URIs for encrypted audio segments at each quality level.
- DRM License: The client requests a DRM license from the License Server, providing its device certificate. The server issues a time-limited key encrypted for the device's hardware security module.
- Segment Streaming: The client begins requesting audio segments from the CDN. Each segment is approximately 1 to 2 seconds of audio. The client monitors throughput and adjusts quality up or down as network conditions change.
- Playback Reporting: The client periodically reports playback events including start, pause, skip, quality changes, and buffer health back to the server via the API for analytics and royalty tracking.
Event-Driven Background Flows
Behind the user-facing request path, a rich ecosystem of event-driven processes continuously operates. Every playback event is published to a Kafka topic, where multiple consumers process it in parallel: the royalty calculator tallies per-stream counts, the recommendation engine updates user preference models, the analytics pipeline populates real-time dashboards, the fraud detection system identifies artificial streaming, and the notification service alerts artists about milestones. This event-driven architecture decouples the hot path (audio delivery) from background processing, ensuring that playback latency is never impacted by downstream processing.
5. Audio Encoding and Codec Selection
Audio encoding is the foundation of a music streaming service. The choice of codec affects audio quality, bandwidth consumption, licensing costs, device compatibility, and battery life. Every major streaming platform has made deliberate codec choices based on their specific constraints, and understanding these choices is essential for designing our system.
Codec Comparison
| Codec | Type | Bitrate Range | Quality | Licensing | Use Case |
|---|---|---|---|---|---|
| AAC-LC | Lossy | 64-320 kbps | Excellent at 128+ kbps | Fraunhofer patent pool | Apple Music, primary streaming |
| AAC-HE v2 | Lossy | 24-64 kbps | Good at very low rates | Fraunhofer patent pool | Low-bandwidth mobile |
| Ogg Vorbis | Lossy | 64-320 kbps | Excellent, surpasses MP3 | Open, royalty-free | Spotify, open platforms |
| Opus | Lossy | 6-510 kbps | Best-in-class at all rates | Open, royalty-free | WebRTC, Discord, YouTube |
| MP3 | Lossy | 128-320 kbps | Good (aging) | Patents expired 2017 | Legacy compatibility |
| FLAC | Lossless | 800-1400 kbps | Perfect (bit-identical) | Open, royalty-free | Tidal HiFi, Apple Lossless |
| ALAC | Lossless | 800-1400 kbps | Perfect (bit-identical) | Open, royalty-free | Apple Lossless tier |
| Dolby Atmos (ADM) | Object-based | Up to 5 Mbps | Spatial, immersive | Dolby licensing | Spatial Audio premium tier |
Our Encoding Pipeline
Our system encodes every incoming track into six quality tiers to cover the full spectrum of network conditions and user preferences. The original lossless master (WAV or FLAC from the label) serves as the source of truth. Each encoding pass is non-destructive and can be re-run with updated encoder versions without re-ingesting the source.
C#
public class AudioEncodingPipeline
{
private readonly ITranscodingService _transcoder;
private readonly ILoudnessNormalizer _normalizer;
public async Task<EncodingResult> EncodeTrackAsync(
TrackMaster master, EncodingProfile profile)
{
// Step 1: Normalize loudness to -14 LUFS (Spotify standard)
var normalized = await _normalizer.NormalizeAsync(
master.AudioStream,
targetLufs: -14.0,
truePeak: -1.0);
// Step 2: Encode to each quality tier
var variants = new List<AudioVariant>();
foreach (var tier in profile.QualityTiers)
{
var encoded = await _transcoder.EncodeAsync(
normalized,
codec: tier.Codec,
bitrate: tier.Bitrate,
sampleRate: tier.SampleRate,
channelLayout: tier.ChannelLayout);
variants.Add(new AudioVariant
{
Tier = tier.Name,
Codec = tier.Codec,
Bitrate = tier.Bitrate,
Url = await UploadToOriginAsync(encoded),
DurationMs = encoded.DurationMs,
Checksum = encoded.Sha256Hash
});
}
return new EncodingResult
{
TrackId = master.TrackId,
Variants = variants,
MasterChecksum = master.Sha256Hash,
EncodedAt = DateTime.UtcNow
};
}
}
public class EncodingProfile
{
public static EncodingProfile DefaultStreaming { get; } = new()
{
QualityTiers = new[]
{
new QualityTier("low", "aac", 64_000, 22050, "mono"),
new QualityTier("medium", "aac", 128_000, 44100, "stereo"),
new QualityTier("high", "aac", 160_000, 44100, "stereo"),
new QualityTier("very_high", "aac", 256_000, 44100, "stereo"),
new QualityTier("lossless", "flac", 800_000, 44100, "stereo"),
new QualityTier("hires", "flac", 1400_000, 96000, "stereo"),
}
};
}
Loudness Normalization
Loudness normalization is a critical but often overlooked step in the encoding pipeline. Without it, listeners would experience jarring volume differences between tracks, a quiet jazz recording followed by a heavily compressed EDM track could force users to constantly adjust their volume. Our system normalizes all tracks to -14 LUFS (Loudness Units Full Scale) using the EBU R128 algorithm, which measures perceived loudness rather than peak amplitude. This ensures consistent perceived volume across the entire catalog. The normalization metadata is embedded in the audio file headers and also stored in the catalog so that clients can apply fine-grained adjustments.
The encoding pipeline processes approximately 100,000 new tracks per day, with each track requiring six encoding passes. At an average encoding time of 2x real-time (a 4-minute track takes 8 minutes to encode), the pipeline requires approximately 500 CPU-cores running continuously. We implement this as a Kubernetes-based auto-scaling job queue, where encoding jobs are distributed across a fleet of worker pods. Each worker pulls an encoding job from the queue, processes it, uploads the encoded variants to origin storage (S3), and publishes a completion event to Kafka. The manifest generator service listens for these events and updates the track's streaming manifest so the new encodings are immediately available to clients.
6. Adaptive Bitrate Streaming (HLS and DASH)
Adaptive bitrate streaming (ABR) is the technology that enables seamless playback across wildly varying network conditions. Instead of downloading an entire audio file and risking buffer exhaustion on slow connections, ABR splits the audio into small segments (typically 1 to 4 seconds each) encoded at multiple quality levels. The client continuously monitors its download speed and buffer health, requesting segments from the quality tier that maximizes quality without risking stalls. This is the same fundamental technology behind Netflix video streaming, but adapted for the lower-latency requirements of audio playback.
HLS vs DASH
| Feature | HLS (HTTP Live Streaming) | MPEG-DASH |
|---|---|---|
| Segment Format | MPEG-TS or fMP4 | fMP4 (ISOBMFF) |
| Manifest Format | M3U8 (text-based) | MPD (XML) |
| DRM Support | FairPlay Streaming (Apple) | Widevine (Google), PlayReady (MS) |
| Platform Support | iOS (native), Android, web (HLS.js) | Android (native), web (dash.js), Smart TVs |
| Segment Duration | 1 to 6 seconds | 1 to 10 seconds |
| Low-Latency Mode | LL-HLS (sub-second) | LL-DASH (sub-second) |
| Adoption | Dominant on iOS/Apple, growing on Android | Dominant on Android/web/Smart TVs |
How Adaptive Bitrate Works in Practice
When a user initiates playback, the client first downloads the master manifest, which lists all available quality variants. For a typical track, this might look like the following M3U8 master playlist:
M3U8
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.2",CHANNELS="1"
/streams/track123/low/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=128000,CODECS="mp4a.40.2",CHANNELS="2"
/streams/track123/medium/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=160000,CODECS="mp4a.40.2",CHANNELS="2"
/streams/track123/high/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=256000,CODECS="mp4a.40.2",CHANNELS="2"
/streams/track123/very_high/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,CODECS="flac",CHANNELS="2"
/streams/track123/lossless/playlist.m3u8
The client's ABR algorithm then makes decisions based on three signals: (1) measured throughput over the last N segment downloads, (2) current buffer health (how many seconds of audio are buffered), and (3) bitrate switching history (to avoid oscillating between quality levels). The algorithm is typically a modified BOLA (Buffer Occupancy based Lyapunov Algorithm) or MPC (Model Predictive Control) approach that optimizes for high quality while keeping buffer occupancy above a safety threshold.
C#
public class AdaptiveBitrateController
{
private readonly Queue<double> _throughputHistory = new();
private int _currentTierIndex;
private double _bufferLevelSeconds;
private const double BufferSafetyThreshold = 5.0;
private const int MaxHistorySamples = 10;
public QualityTier SelectNextTier(
QualityTier[] availableTiers,
double measuredThroughputBps,
double bufferLevelSeconds)
{
_bufferLevelSeconds = bufferLevelSeconds;
_throughputHistory.Enqueue(measuredThroughputBps);
if (_throughputHistory.Count > MaxHistorySamples)
_throughputHistory.Dequeue();
double smoothedThroughput = _throughputHistory.Average();
// Apply safety margin: use 80% of measured throughput
double safeThroughput = smoothedThroughput * 0.8;
// If buffer is critically low, drop to lowest tier immediately
if (_bufferLevelSeconds < 2.0)
{
_currentTierIndex = 0;
return availableTiers[0];
}
// Find highest tier that fits within safe throughput
int bestTierIndex = 0;
for (int i = availableTiers.Length - 1; i >= 0; i--)
{
if (availableTiers[i].Bitrate <= safeThroughput)
{
bestTierIndex = i;
break;
}
}
// Only upgrade one tier at a time to avoid oscillation
if (bestTierIndex > _currentTierIndex + 1)
bestTierIndex = _currentTierIndex + 1;
_currentTierIndex = bestTierIndex;
return availableTiers[bestTierIndex];
}
}
Segment Architecture
Audio segments are the atomic units of streaming delivery. Each segment contains exactly 1 to 2 seconds of audio in an MPEG-TS or fragmented MP4 container. Segments are numbered sequentially (segment 0, segment 1, etc.) and referenced by byte offset in the variant playlist. When a client requests a segment, the CDN serves the pre-encoded chunk from edge cache. Because segments are immutable once created, they are perfectly cacheable. This is the key insight that makes audio CDNs so efficient. A popular track might have millions of segment requests per day, all served from edge cache with zero origin hits.
The segment duration represents a critical tradeoff: shorter segments (1 second) enable faster quality switching and lower latency for live streaming, but increase HTTP overhead because each segment requires a separate request. Longer segments (4 to 6 seconds) reduce HTTP overhead and improve CDN cache efficiency, but slow down quality adaptation and increase the minimum playback latency. For music streaming, we use 2-second segments as the optimal balance point. For live audio (live radio, live sessions), we use 1-second segments with LL-HLS for sub-2-second end-to-end latency.
7. Audio CDN Architecture
The Content Delivery Network (CDN) is the backbone of music streaming. With 50 million concurrent streams each transferring 20 KB/s of audio data, the CDN must sustain a throughput of 1 terabyte per second across hundreds of edge locations worldwide. No single CDN provider can cost-effectively serve this volume, so we employ a multi-CDN strategy with intelligent request routing. This approach provides both cost optimization (bidding between providers) and resilience (failover when one provider has an outage).
Multi-CDN Strategy
| Provider | Role | Edge Locations | Strengths |
|---|---|---|---|
| CloudFront (AWS) | Primary CDN | 450+ PoPs | Largest footprint, S3 origin integration |
| Fastly | Secondary CDN | 90+ PoPs | Real-time purge, edge compute (Wasm) |
| Cloudflare | Tertiary / DDoS | 300+ PoPs | DDoS protection, DNS, edge caching |
| Google Cloud CDN | GCP origin shield | 180+ PoPs | Android/Google ecosystem, low-latency |
Request Routing Architecture
Client requests do not go directly to a CDN provider. Instead, they hit our CDN Router service, which makes real-time routing decisions based on provider performance, cost, and current load. The router maintains a continuously updated performance map by analyzing real-time telemetry from all CDN providers (latency, error rate, throughput per region). It uses a weighted scoring algorithm to route each request to the optimal provider.
C#
public class CdnRouter
{
private readonly ICdnPerformanceMonitor _monitor;
private readonly Dictionary<string, double> _providerWeights = new();
public async Task<string> RouteSegmentRequest(
string segmentId, string clientRegion)
{
var providers = await _monitor
.GetProviderScoresAsync(clientRegion);
// Score = (1 / latency_p95) * (1 - error_rate) * cost_weight
var bestProvider = providers
.OrderByDescending(p =>
(1.0 / p.LatencyP95Ms) *
(1.0 - p.ErrorRate) *
p.CostWeight)
.First();
if (bestProvider.Score < MinimumQualityThreshold)
{
// All providers degraded, serve from local cache
// or use direct origin fallback
return await FallbackToOriginAsync(segmentId);
}
_providerWeights[bestProvider.Name] = bestProvider.Score;
return bestProvider.GetSegmentUrl(segmentId);
}
}
Origin Shield Architecture
To protect our origin storage (S3) from thundering herd effects during cache misses, we deploy an origin shield tier. The origin shield is a mid-tier cache that sits between the CDN edge nodes and the S3 origin. When an edge node experiences a cache miss, it first checks the origin shield before hitting S3. This two-tier caching architecture dramatically reduces origin load: instead of 500+ edge nodes potentially hitting S3 simultaneously for the same segment, only one edge node hits the shield, and the shield deduplicates concurrent requests for the same segment. This reduces S3 request volume by 90%+ and eliminates cache stampede events that can occur when a new track is released and millions of users request the same segments simultaneously.
Cache Warming Strategy
For new album releases, we proactively warm CDN caches before the release time. When a label schedules a release for midnight UTC, our system pre-pushes the encoded audio segments to all CDN edge locations at 11:30 PM UTC. This ensures that when millions of fans hit play at midnight, every segment is already cached at the nearest edge, resulting in sub-50ms segment delivery times. Without cache warming, the first users to request a track would experience slow startup times as segments trickle into the cache from origin. The cache warming pipeline processes approximately 500 new releases per day during peak periods (Friday new music drops), pushing an average of 10 GB of audio data per release across 200+ edge locations. This requires approximately 2 TB of data transfer per warming cycle, which completes within 30 minutes using parallelized HTTP PUT operations.
8. Music Catalog Management
The music catalog is the central nervous system of the streaming platform. It maintains authoritative records for 100+ million tracks, including audio file references, metadata, rights information, availability rules, and playback policies. The catalog must support high-throughput reads (115K+ QPS average) while accepting continuous writes from the content ingestion pipeline (100K+ tracks per day of updates). This read-heavy workload pattern demands a carefully designed storage and caching architecture.
Catalog Data Model
SQL
CREATE TABLE tracks (
track_id UUID PRIMARY KEY,
isrc VARCHAR(12) UNIQUE NOT NULL,
title VARCHAR(500) NOT NULL,
duration_ms INTEGER NOT NULL,
album_id UUID NOT NULL REFERENCES albums(album_id),
disc_number SMALLINT DEFAULT 1,
track_number SMALLINT,
explicit BOOLEAN DEFAULT FALSE,
has_lyrics BOOLEAN DEFAULT FALSE,
loudness_lufs DECIMAL(5,2),
key_signature VARCHAR(10),
tempo_bpm SMALLINT,
is_compilation BOOLEAN DEFAULT FALSE,
content_rating VARCHAR(20) DEFAULT 'clean',
available_markets TEXT[],
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE albums (
album_id UUID PRIMARY KEY,
upc VARCHAR(14) UNIQUE NOT NULL,
title VARCHAR(500) NOT NULL,
album_type VARCHAR(20) NOT NULL,
release_date DATE NOT NULL,
label_id UUID REFERENCES labels(label_id),
total_discs SMALLINT DEFAULT 1,
cover_art_url VARCHAR(1000),
genres TEXT[],
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE artists (
artist_id UUID PRIMARY KEY,
name VARCHAR(500) NOT NULL,
verified BOOLEAN DEFAULT FALSE,
bio TEXT,
image_url VARCHAR(1000),
monthly_listeners INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE track_artists (
track_id UUID REFERENCES tracks(track_id),
artist_id UUID REFERENCES artists(artist_id),
role VARCHAR(30) NOT NULL,
order_index SMALLINT DEFAULT 0,
PRIMARY KEY (track_id, artist_id, role)
);
CREATE TABLE audio_files (
file_id UUID PRIMARY KEY,
track_id UUID NOT NULL REFERENCES tracks(track_id),
codec VARCHAR(20) NOT NULL,
bitrate INTEGER NOT NULL,
sample_rate INTEGER NOT NULL,
channel_layout VARCHAR(20) NOT NULL,
file_size_bytes BIGINT NOT NULL,
checksum_sha256 VARCHAR(64) NOT NULL,
origin_key VARCHAR(500) NOT NULL,
cdn_urls JSONB,
encoded_at TIMESTAMPTZ NOT NULL
);
CREATE INDEX idx_tracks_isrc ON tracks(isrc);
CREATE INDEX idx_tracks_album ON tracks(album_id, track_number);
CREATE INDEX idx_albums_release ON albums(release_date DESC);
CREATE INDEX idx_artists_name ON artists USING gin(to_tsvector('english', name));
CREATE INDEX idx_audio_track_bitrate ON audio_files(track_id, bitrate);
Catalog Caching Strategy
The catalog employs a multi-layer caching strategy to handle the read-heavy workload. The first layer is the application-level in-memory cache (ConcurrentDictionary with LRU eviction) on each API server, holding metadata for the 100,000 most recently accessed tracks. The second layer is a distributed Redis cluster holding metadata for the 1 million most popular tracks (covering approximately 80% of all playback requests). The third layer is the PostgreSQL primary database with read replicas. This three-tier approach ensures that the vast majority of catalog reads are served from memory, with sub-millisecond latency, and only the long tail of less popular tracks requires database queries.
C#
public class CatalogService
{
private readonly IMemoryCache _localCache;
private readonly IDistributedCache _redisCache;
private readonly CatalogRepository _db;
public async Task<TrackMetadata> GetTrackAsync(Guid trackId)
{
// Layer 1: In-memory LRU cache (sub-microsecond)
if (_localCache.TryGetValue(trackId, out TrackMetadata cached))
return cached;
// Layer 2: Redis distributed cache (approximately 1ms)
var redisKey = $"track:{trackId}";
var redisData = await _redisCache.GetStringAsync(redisKey);
if (redisData != null)
{
var fromRedis = JsonSerializer
.Deserialize<TrackMetadata>(redisData);
_localCache.Set(trackId, fromRedis,
TimeSpan.FromMinutes(5));
return fromRedis;
}
// Layer 3: PostgreSQL (approximately 5ms with connection pooling)
var fromDb = await _db.GetTrackAsync(trackId);
if (fromDb != null)
{
await _redisCache.SetStringAsync(redisKey,
JsonSerializer.Serialize(fromDb),
new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow =
TimeSpan.FromHours(1)
});
_localCache.Set(trackId, fromDb,
TimeSpan.FromMinutes(5));
}
return fromDb;
}
}
9. Metadata and Tagging System
Music metadata goes far beyond basic artist and track names. A comprehensive metadata system captures dozens of attributes per track, enabling rich search, filtering, editorial curation, and recommendation. Our metadata system uses a hybrid approach: structured fields (genre, BPM, key, release date) are stored as typed columns in PostgreSQL, while flexible fields (mood tags, activity tags, custom labels) are stored as JSONB properties with GIN index support for fast querying.
Metadata Taxonomy
| Category | Fields | Source | Update Frequency |
|---|---|---|---|
| Core Identity | ISRC, title, artist names, album title | Distributor feed | On ingestion |
| Technical Audio | Duration, bitrate, sample rate, loudness | Audio analysis | On encoding |
| Musical Analysis | Key, tempo (BPM), time signature, mode | ML analysis pipeline | On ingestion |
| Classification | Genre, sub-genre, era, language | Label metadata + ML | On ingestion + periodic |
| Mood and Activity | Mood tags (energetic, calm, sad), activity (workout, study, sleep) | ML model + editorial | Weekly retraining |
| Content Descriptors | Instrumentation, vocal type, song structure | ML audio analysis | On ingestion |
| Business | Rights holders, publishing splits, territorial rights | Label/licensor data | On ingestion + updates |
| Editorial | Featured placement, mood boards, curated tags | Editorial team | Ad hoc |
Automated Metadata Enrichment
Raw metadata from distributors is often incomplete or inconsistent. Our enrichment pipeline uses a combination of audio analysis (extracting technical features like key, BPM, loudness, and spectral characteristics), ML classification (genre classification, mood tagging, activity recommendation), and cross-referencing with established databases (MusicBrainz for ISRC/UPC validation, AcoustID for fingerprint matching). This pipeline runs automatically for every newly ingested track and can reprocess the entire catalog when ML models are updated.
C#
public class MetadataEnrichmentService
{
private readonly IAudioAnalyzer _audioAnalyzer;
private readonly IGenreClassifier _genreClassifier;
private readonly IMoodAnalyzer _moodAnalyzer;
private readonly IMusicBrainzClient _musicBrainz;
public async Task<EnrichedMetadata> EnrichTrackAsync(
TrackMaster master, RawMetadata rawMeta)
{
// Step 1: Validate external identifiers
var validated = await _musicBrainz.ValidateAsync(
rawMeta.Isrc, rawMeta.Upc);
// Step 2: Audio feature extraction
var audioFeatures = await _audioAnalyzer.AnalyzeAsync(
master.AudioStream);
// Step 3: ML-based genre classification
var genres = await _genreClassifier.ClassifyAsync(
audioFeatures.MfccVector,
audioFeatures.ChromaVector);
// Step 4: Mood and activity tagging
var moodTags = await _moodAnalyzer.AnalyzeAsync(
audioFeatures);
return new EnrichedMetadata
{
TrackId = master.TrackId,
Isrc = validated.Isrc,
Key = audioFeatures.DetectedKey,
Tempo = audioFeatures.EstimatedBpm,
LoudnessLufs = audioFeatures.IntegratedLoudness,
Genres = genres,
SubGenre = genres.Primary,
Moods = moodTags.Moods,
Activities = moodTags.Activities,
Energy = audioFeatures.EnergyScore,
Danceability = audioFeatures.DanceScore,
Valence = audioFeatures.ValenceScore,
Instrumentalness = audioFeatures.InstrumentalScore,
EnrichedAt = DateTime.UtcNow
};
}
}
The audio analysis pipeline is built on librosa-equivalent C# DSP libraries (NAudio for raw audio processing, Math.NET for signal analysis) and runs on GPU-accelerated Kubernetes pods. The MFCC (Mel-Frequency Cepstral Coefficients) extraction and chroma vector computation are the most computationally expensive steps, taking approximately 3 to 5 seconds per track on a GPU. The entire enrichment pipeline processes a track in under 30 seconds, compared to the several minutes required for encoding. This means metadata is available before the encoded audio files are uploaded to the CDN, enabling immediate search indexing and recommendation model updates.
10. Search and Discovery Engine
Search is the primary discovery mechanism for most users. When a user types "bohemian rhapsody" into the search bar, they expect near-instant results across tracks, artists, albums, and playlists, even with typos, alternate spellings, and transliterated text. Our search engine is built on Elasticsearch with custom analyzers tuned for music-specific queries, semantic search powered by vector embeddings, and a result ranking algorithm that balances relevance, popularity, and user personalization.
Search Architecture
Elasticsearch Index Schema
JSON
{
"mappings": {
"properties": {
"track_id": { "type": "keyword" },
"title": { "type": "text", "analyzer": "music_analyzer", "fields": {
"keyword": { "type": "keyword", "ignore_above": 512 },
"suggest": { "type": "completion", "analyzer": "simple" }
}},
"artist_name": { "type": "text", "analyzer": "music_analyzer", "fields": {
"keyword": { "type": "keyword" },
"suggest": { "type": "completion", "analyzer": "simple" }
}},
"album_title": { "type": "text", "analyzer": "music_analyzer" },
"genres": { "type": "keyword" },
"moods": { "type": "keyword" },
"release_date":{ "type": "date" },
"popularity": { "type": "integer" },
"explicit": { "type": "boolean" },
"duration_ms": { "type": "integer" },
"available_markets": { "type": "keyword" },
"title_embedding": {
"type": "dense_vector",
"dims": 384,
"index": true,
"similarity": "cosine"
}
}
},
"settings": {
"analysis": {
"analyzer": {
"music_analyzer": {
"tokenizer": "standard",
"filter": ["lowercase", "music_synonym", "asciifolding"]
}
},
"filter": {
"music_synonym": {
"type": "synonym",
"synonyms": [
"rock, rock music, rock'n'roll",
"hip hop, hip-hop, rap",
"electronic, edm, electronica"
]
}
}
}
}
}
Result Ranking Algorithm
Search results are ranked using a multi-signal scoring model that considers text relevance (BM25 score), popularity (monthly stream count, normalized), freshness (recency boost for new releases), personalization (user's genre preferences, listening history), and editorial priority (featured tracks get a ranking boost). The final score is a weighted combination: Score equals 0.40 times Relevance plus 0.25 times Popularity plus 0.15 times Personalization plus 0.10 times Freshness plus 0.10 times Editorial. These weights are continuously optimized through A/B testing and click-through rate analysis.
Autocomplete is powered by a Redis-backed trie structure that stores the top 10 million popular query prefixes. When a user types, the client debounces keystrokes and sends a prefix query to the autocomplete endpoint every 200ms. The autocomplete service performs a Redis ZRANGEBYSCORE operation to return the top 10 completions sorted by popularity. This provides sub-10ms latency for autocomplete suggestions, which is essential for the responsive search experience users expect. Semantic search using vector embeddings enables fuzzy matching: a user searching for "sad songs for rainy days" can find relevant tracks even if none of those exact words appear in the metadata, because the query embedding is matched against pre-computed track embeddings using approximate nearest neighbor (ANN) search with HNSW indexing.
11. Playlist Management System
Playlists are the primary content organization mechanism in music streaming. Users create personal playlists, editorial teams curate platform playlists, and algorithms generate personalized playlists. The playlist system must handle millions of playlists with varying sizes (from 5 tracks to 10,000+ tracks), support real-time collaboration between multiple users, enable seamless transitions between playlist tracks during playback, and integrate with the recommendation engine for smart playlist suggestions.
Playlist Data Model
SQL
CREATE TABLE playlists (
playlist_id UUID PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES users(user_id),
title VARCHAR(500) NOT NULL,
description TEXT,
cover_image_url VARCHAR(1000),
visibility VARCHAR(20) DEFAULT 'private',
playlist_type VARCHAR(20) DEFAULT 'user',
follower_count INTEGER DEFAULT 0,
track_count INTEGER DEFAULT 0,
total_duration_ms BIGINT DEFAULT 0,
is_snapshot BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE playlist_tracks (
playlist_id UUID REFERENCES playlists(playlist_id),
track_id UUID REFERENCES tracks(track_id),
added_by UUID REFERENCES users(user_id),
position INTEGER NOT NULL,
added_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (playlist_id, position)
);
CREATE TABLE playlist_collaborators (
playlist_id UUID REFERENCES playlists(playlist_id),
user_id UUID REFERENCES users(user_id),
role VARCHAR(20) DEFAULT 'editor',
invited_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (playlist_id, user_id)
);
CREATE INDEX idx_playlist_owner
ON playlists(owner_id, updated_at DESC);
CREATE INDEX idx_playlist_tracks_track
ON playlist_tracks(track_id);
CREATE INDEX idx_playlist_visibility
ON playlists(visibility, follower_count DESC);
Collaborative Playlist Editing
Collaborative playlists require careful concurrency handling. When multiple users add or reorder tracks simultaneously, we use optimistic concurrency control with version vectors. Each edit operation includes the expected version number; if the version has changed since the client read it, the operation is rejected with a conflict response, and the client must merge the changes. In practice, edit conflicts are rare (collaborative playlists typically have 2 to 5 collaborators editing occasionally), so optimistic concurrency works well without the overhead of distributed locking.
C#
public class PlaylistService
{
private readonly PlaylistRepository _db;
private readonly IDistributedCache _cache;
private readonly IEventBus _events;
public async Task<PlaylistEditResult> AddTrackAsync(
Guid playlistId, Guid trackId, Guid userId,
int position)
{
var playlist = await _db.GetPlaylistAsync(playlistId);
if (playlist == null)
throw new PlaylistNotFoundException(playlistId);
// Check authorization
if (playlist.Visibility == "collaborative")
{
var isCollab = await _db.IsCollaboratorAsync(
playlistId, userId);
if (!isCollab)
throw new UnauthorizedException(
"User is not a collaborator");
}
else if (playlist.OwnerId != userId)
{
throw new UnauthorizedException(
"Only owner can edit non-collaborative playlists");
}
// Optimistic concurrency check
var result = await _db.AddTrackToPlaylistAsync(
playlistId, trackId, userId, position,
expectedVersion: playlist.Version);
if (result == EditResult.Conflict)
{
return PlaylistEditResult.Conflict(
"Playlist was modified by another user. " +
"Please refresh and try again.");
}
// Update caches and publish event
await _cache.RemoveAsync($"playlist:{playlistId}");
await _events.PublishAsync(
new TrackAddedToPlaylist(
playlistId, trackId, userId));
return PlaylistEditResult.Success();
}
}
Algorithmic Playlists
Algorithmic playlists like "Discover Weekly" and "Release Radar" are generated by a batch pipeline that runs nightly. The pipeline fetches each user's listening history, computes a personalized track distribution using collaborative filtering and content-based similarity, samples tracks according to the distribution, and writes the resulting playlist to the playlist service. The generation pipeline uses Apache Spark for distributed processing and serves 300 million users, generating approximately 1 billion personalized tracks per night. Each user's Discover Weekly contains 30 tracks, requiring roughly 9 billion track-to-user assignments in a single nightly batch. The pipeline must complete within a 4-hour window (12 AM to 4 AM) to ensure playlists are ready when users wake up, requiring approximately 125,000 parallel Spark tasks across a 5,000-node cluster.
12. Recommendation Engine
The recommendation engine is the most complex machine learning system in the platform and arguably the most important for user retention. It drives personalized home pages, radio stations, "Fans Also Like" suggestions, and playlist recommendations. The engine combines multiple ML models with different signal sources and latency characteristics, orchestrated by a serving layer that makes real-time decisions about which model's suggestions to present.
Recommendation Architecture
Two-Tower Deep Retrieval Model
The core of our recommendation system is a two-tower neural network that learns dense vector embeddings for both users and tracks. The user tower processes the user's recent listening history (last 200 tracks), demographic features, and explicit preferences to produce a 256-dimensional user embedding. The track tower processes audio features, metadata features, and collaborative signals to produce a 256-dimensional track embedding. During serving, the system computes the dot product between the user embedding and all track embeddings to find the most similar tracks. This is implemented using approximate nearest neighbor (ANN) search with the ScaNN library, which enables sub-10ms retrieval from a pool of 100 million track embeddings.
C#
public class RecommendationService
{
private readonly IUserEmbeddingStore _userEmbeddings;
private readonly ITrackEmbeddingIndex _trackIndex;
private readonly IRankingModel _rankingModel;
private readonly IFeatureStore _features;
public async Task<List<Recommendation>>
GetRecommendationsAsync(
Guid userId, RecommendationContext context)
{
// Step 1: Get user embedding (pre-computed, cached in Redis)
var userEmbedding = await _userEmbeddings
.GetUserEmbeddingAsync(userId);
// Step 2: Candidate generation via ANN search
// Return 500 candidates from 100M track pool in approximately 5ms
var candidates = await _trackIndex
.SearchAsync(userEmbedding, topK: 500);
// Step 3: Filter out already-played tracks
var playedTracks = await _features
.GetRecentlyPlayedAsync(userId, days: 30);
candidates = candidates
.Where(c => !playedTracks.Contains(c.TrackId))
.ToList();
// Step 4: Fine-grained ranking with cross-features
var features = await _features
.GetCandidateFeaturesAsync(
userId, candidates, context);
var ranked = await _rankingModel.RankAsync(features);
// Step 5: Apply business rules
var diversified = ApplyDiversityRules(ranked,
maxConsecutiveSameArtist: 2,
minGenreDiversity: 3,
freshReleaseBoost: 0.15);
return diversified.Take(30).ToList();
}
}
Model Training Pipeline
Model training is a continuous process. The collaborative filtering model (Alternating Least Squares) retrains nightly on the full user-item interaction matrix (approximately 50 billion interactions). The two-tower model retrains weekly on GPU clusters using a contrastive learning objective (sampled softmax loss). The sequential Transformer model retrains bi-weekly on user session data. All models are evaluated offline using precision at K, recall at K, and NDCG metrics, and online through A/B testing with engagement metrics (skip rate, save rate, playlist add rate). A model is only promoted to production if it shows statistically significant improvement over the current champion in a 7-day A/B test with at least 1% of users.
14. Offline Playback and DRM
Offline playback is a critical feature for users in areas with unreliable connectivity such as subways, airplanes, and rural areas. The system must download tracks for offline listening while preventing piracy through Digital Rights Management (DRM). The DRM system must be transparent to the user (seamless download and playback) while robust enough to satisfy record label requirements for content protection.
Download and DRM Architecture
DRM Implementation
Our DRM system uses platform-native encryption: Widevine (Google) for Android and Chrome, FairPlay (Apple) for iOS and Safari, and PlayReady (Microsoft) for Windows and Edge. The encryption uses AES-128-CBC for the audio segments, with the encryption key delivered through the platform's license server protocol. The client-side DRM module (integrated into the player) decrypts audio segments in memory immediately before decoding, ensuring that decrypted audio never touches the filesystem.
C#
public class OfflineDownloadService
{
private readonly IDrmLicenseClient _drmClient;
private readonly IStorageService _storage;
private readonly ISubscriptionValidator _subscription;
public async Task<DownloadResult> DownloadTrackAsync(
Guid userId, Guid trackId, Guid deviceId)
{
// Step 1: Verify subscription allows offline
var subscription = await _subscription
.GetSubscriptionAsync(userId);
if (!subscription.AllowsOfflinePlayback)
throw new FeatureUnavailableException(
"Offline playback requires Premium subscription");
// Step 2: Check device limit (max 5 devices)
var deviceCount = await _storage
.GetDownloadedTrackCountAsync(userId, deviceId);
if (deviceCount >= MaxOfflineDevices)
throw new DeviceLimitExceededException(
"Maximum offline devices reached.");
// Step 3: Request offline DRM license
var license = await _drmClient
.RequestOfflineLicenseAsync(
trackId: trackId,
deviceId: deviceId,
validUntil: DateTime.UtcNow.AddDays(30));
// Step 4: Download encrypted audio variants
var track = await _catalog.GetTrackAsync(trackId);
var selectedTier = SelectTierForOffline(
subscription.QualityTier,
track.AvailableVariants);
var encryptedAudio = await _cdn
.DownloadSegmentedAsync(
selectedTier.CdnManifestUrl);
// Step 5: Store encrypted content + license locally
await _storage.SaveOfflineTrackAsync(
userId: userId,
trackId: trackId,
deviceId: deviceId,
encryptedAudio: encryptedAudio,
license: license,
metadata: track.Metadata);
return DownloadResult.Success(trackId);
}
private QualityTier SelectTierForOffline(
SubscriptionTier subTier,
AudioVariant[] available)
{
return subTier switch
{
SubscriptionTier.Premium => available
.First(v => v.Bitrate == 320_000),
SubscriptionTier.Standard => available
.First(v => v.Bitrate == 160_000),
_ => available
.First(v => v.Bitrate == 128_000)
};
}
}
Offline License Renewal
Offline DRM licenses expire periodically (typically 30 days) to ensure users remain subscribers and to enable remote license revocation if content is removed from the catalog. When a device is online, the DRM module silently renews licenses for all downloaded tracks. When offline, playback continues using the cached license until it expires. If a license expires while offline, the tracks become unplayable until the device reconnects and renews. The license renewal process is backgrounded and does not interrupt playback. The client proactively renews licenses when they have 7 days remaining validity.
15. Lyrics System
Lyrics are a high-engagement feature that deepens the listening experience. Our lyrics system supports time-synchronized lyrics (displayed word-by-word in sync with the audio), static lyrics (full text display), and translated lyrics for international audiences. The system must handle multiple lyrics providers, cache aggressively (lyrics are read for every track playback), and gracefully handle missing or inaccurate lyrics.
Lyrics Data Model
SQL
CREATE TABLE lyrics (
lyrics_id UUID PRIMARY KEY,
track_id UUID NOT NULL REFERENCES tracks(track_id),
provider VARCHAR(50) NOT NULL,
language VARCHAR(10) NOT NULL,
is_timed BOOLEAN NOT NULL,
content TEXT NOT NULL,
is_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(track_id, provider, language)
);
CREATE TABLE lyrics_translations (
translation_id UUID PRIMARY KEY,
lyrics_id UUID NOT NULL REFERENCES lyrics(lyrics_id),
target_language VARCHAR(10) NOT NULL,
content TEXT NOT NULL,
provider VARCHAR(50) NOT NULL
);
CREATE INDEX idx_lyrics_track
ON lyrics(track_id, language);
CREATE INDEX idx_lyrics_timed
ON lyrics(track_id) WHERE is_timed = TRUE;
Time-Synchronized Lyrics Format (LRC)
LRC
[00:12.00]Is this the real life?
[00:15.30]Is this just fantasy?
[00:19.10]Caught in a landslide
[00:22.80]No escape from reality
[00:27.50]Open your eyes
[00:30.20]Look up to the skies and see
[00:35.00]I'm just a poor boy
[00:38.50]I need no sympathy
[00:42.00]Because I'm easy come, easy go
[00:47.00]Little high, little low
[00:51.00]Any way the wind blows
[00:55.00]Doesn't really matter to me, to me
The lyrics display client receives the LRC content and parses it into a list of timestamp-text pairs. During playback, the client interpolates the current position to highlight the active line and smoothly scroll to the next line at the appropriate time. For word-by-word synchronization (karaoke-style), we use an extended LRC format with per-word timestamps, provided by premium lyrics partners.
Lyrics Provider Strategy
We aggregate lyrics from multiple providers to maximize coverage. Musixmatch provides the largest timed lyrics database (over 14 million tracks). Genius provides annotated lyrics and song meanings. For tracks not covered by commercial providers, we use community-sourced LRC files from open databases. When multiple providers have lyrics for the same track, we prefer timed lyrics over static, and verified (editorially checked) lyrics over unverified. Lyrics are cached in Redis with a 24-hour TTL and in the client app for offline availability. The lyrics API returns a combined response that includes the best available lyrics in the user's preferred language, falling back to the original language if a translation is not available.
16. Podcast and Long-Form Audio Support
Podcasts and long-form audio (audiobooks, radio shows, live recordings) represent a fundamentally different content type from music tracks. A 2-hour podcast episode cannot be processed the same way as a 3-minute song. The system must support chapters, variable playback speed, transcript search, continuous playback across episodes, and different monetization models (ad insertion, subscription tiers).
Podcast Architecture Differences
| Feature | Music Track | Podcast Episode |
|---|---|---|
| Duration | 2 to 8 minutes | 15 minutes to 4 hours |
| Segment Size | 2 seconds | 10 seconds (reduced HTTP overhead) |
| Encoding | Pre-encoded multiple bitrates | Single bitrate plus voice-optimized Opus |
| Chapters | None | Multiple chapters with timestamps |
| Transcript | Lyrics (optional) | Full transcript (required) |
| Ad Insertion | None | Dynamic mid-roll and pre-roll ads |
| Resume | Resume from last position | Cross-device episode progress sync |
| Playback Speed | 1x only (artistic intent) | 0.5x to 3x adjustable |
Dynamic Ad Insertion (DAI)
Podcast monetization relies on dynamic ad insertion, where the audio segments of an episode contain "ad markers" (positions where advertisements can be dynamically spliced in based on the listener's demographics, location, and time). The podcast manifest includes ad placeholder segments that the server replaces with actual ad audio at serving time. This enables different listeners to hear different ads in the same episode, and enables the same ad slot to be sold to different advertisers based on targeting criteria.
C#
public class PodcastManifestService
{
public async Task<HlsManifest> GenerateManifestAsync(
Guid episodeId, Guid listenerId, AdContext adContext)
{
var episode = await _catalog
.GetEpisodeAsync(episodeId);
var adSlots = await _adService
.GetTargetedAdsAsync(adContext);
var segments = new List<ManifestSegment>();
int adIndex = 0;
foreach (var segment in episode.SegmentList)
{
if (segment.IsAdMarker)
{
// Replace ad marker with targeted ads
if (adIndex < adSlots.Count)
{
var ad = adSlots[adIndex++];
segments.AddRange(ad.AudioSegments);
}
else
{
// No ad available, use silence filler
segments.Add(
SilenceSegment(segment.Duration));
}
}
else
{
segments.Add(segment);
}
}
return BuildHlsManifest(
segments, episode.BitrateVariants);
}
}
17. Artist Analytics Dashboard
The artist analytics dashboard gives musicians real-time insights into how their music is being consumed. Artists can see stream counts, listener demographics, geographic distribution, playlist placements, revenue estimates, and trend analysis. This data must be accurate (artists base financial decisions on it), real-time (artists check during album launches), and privacy-compliant (individual listener data is aggregated, never exposed).
Analytics Data Pipeline
Analytics Data Model
SQL
-- ClickHouse table for stream events
CREATE TABLE stream_events (
event_id UUID,
track_id UUID,
artist_id UUID,
user_id UUID,
user_country LowCardinality(String),
user_age_group LowCardinality(String),
user_gender LowCardinality(String),
stream_source LowCardinality(String),
playlist_id Nullable(UUID),
device_type LowCardinality(String),
start_time DateTime,
end_time Nullable(DateTime),
listen_duration_ms UInt32,
completion_rate Float32,
is_offline Bool,
quality_tier LowCardinality(String)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(start_time)
ORDER BY (artist_id, track_id, start_time);
-- Materialized view for daily artist summaries
CREATE MATERIALIZED VIEW artist_daily_stats
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(day)
ORDER BY (artist_id, track_id, day)
AS SELECT
artist_id,
track_id,
toDate(start_time) AS day,
count() AS stream_count,
sum(listen_duration_ms) AS total_listen_ms,
uniq(user_id) AS unique_listeners,
uniq(user_country) AS countries
FROM stream_events
GROUP BY artist_id, track_id, day;
Real-Time Stream Counter
During an album launch, artists want to see stream counts updating in real time. We achieve this using Apache Flink for real-time aggregation of the stream event Kafka topic, with results written to Redis for sub-millisecond reads. The dashboard polls the Redis counter every 5 seconds and displays a live-updating count. Within the first hour of a major album release, a top artist might accumulate 10 million+ streams, and the counter must reflect this accurately without delays or dropped events. Flink's exactly-once processing semantics guarantee that no stream events are lost or double-counted.
C#
public class ArtistAnalyticsService
{
private readonly IRealTimeCounter _realTimeCounter;
private readonly IAnalyticsRepository _clickHouse;
private readonly IDistributedCache _cache;
public async Task<ArtistDashboard> GetDashboardAsync(
Guid artistId, DateRange range)
{
// Real-time counters (Redis, updated by Flink)
var todayStreams = await _realTimeCounter
.GetAsync($"artist:{artistId}:streams:today");
var todayListeners = await _realTimeCounter
.GetAsync($"artist:{artistId}:listeners:today");
// Historical data (ClickHouse, cached)
var cacheKey = $"analytics:{artistId}:{range}";
var historical = await _cache
.GetOrSetAsync(cacheKey, async () =>
{
return await _clickHouse
.GetArtistStatsAsync(artistId, range);
}, TimeSpan.FromMinutes(15));
return new ArtistDashboard
{
TodayStreams = todayStreams,
TodayListeners = todayListeners,
StreamTrend = historical.DailyStreamCounts,
TopTracks = historical.TopTracks,
GeographicBreakdown = historical.CountryDistribution,
Demographics = historical.AgeGenderDistribution,
PlaylistPlacements = historical.PlaylistPlacements,
EstimatedRevenue = historical.RevenueEstimate
};
}
}
18. Royalty Calculation Engine
Royalty calculation is the financial backbone of a music streaming service. Every stream generates a micro-payment that must be accurately tracked, aggregated, and distributed to rights holders (artists, songwriters, publishers, labels, distributors). The calculation involves complex pro-rata formulas, territory-specific rates, contractual splits, minimum guarantees, and advance recoupment. Errors in royalty calculation can lead to legal disputes, regulatory penalties, and loss of trust from rights holders.
Royalty Calculation Model
SQL
CREATE TABLE royalty_accounts (
account_id UUID PRIMARY KEY,
rights_holder_id UUID NOT NULL,
holder_type VARCHAR(20) NOT NULL,
territory VARCHAR(5) NOT NULL,
contract_id UUID NOT NULL,
pro_rata_share DECIMAL(10,8) NOT NULL,
currency VARCHAR(3) DEFAULT 'USD',
payment_terms VARCHAR(20) DEFAULT 'net30',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE royalty_accruals (
accrual_id UUID PRIMARY KEY,
period_start DATE NOT NULL,
period_end DATE NOT NULL,
territory VARCHAR(5) NOT NULL,
stream_count BIGINT NOT NULL,
total_revenue DECIMAL(15,4) NOT NULL,
per_stream_rate DECIMAL(15,10) NOT NULL,
calculated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE royalty_line_items (
line_item_id UUID PRIMARY KEY,
accrual_id UUID NOT NULL REFERENCES royalty_accruals(accrual_id),
rights_holder_id UUID NOT NULL,
stream_count BIGINT NOT NULL,
share_amount DECIMAL(15,8) NOT NULL,
holdback_pct DECIMAL(5,4) DEFAULT 0,
net_amount DECIMAL(15,8) NOT NULL,
status VARCHAR(20) DEFAULT 'accrued'
);
CREATE TABLE royalty_payments (
payment_id UUID PRIMARY KEY,
rights_holder_id UUID NOT NULL,
period_month DATE NOT NULL,
total_amount DECIMAL(15,2) NOT NULL,
currency VARCHAR(3) NOT NULL,
payment_method VARCHAR(20),
paid_at TIMESTAMPTZ,
status VARCHAR(20) DEFAULT 'pending'
);
Monthly Royalty Calculation Pipeline
The royalty calculation runs as a monthly batch job on Apache Spark. It processes the entire month's stream events (approximately 75 billion events), joins them with contract terms and territory-specific rates, and produces per-rights-holder accruals. The pipeline is designed for idempotency: it can be re-run for any historical month without producing duplicate accruals, using upsert operations against the royalty_line_items table.
C#
public class RoyaltyCalculationEngine
{
public async Task<RoyaltyReport>
CalculateMonthlyRoyaltiesAsync(DateMonth period)
{
// Step 1: Aggregate streams by track, territory,
// and rights holder
var streamAggregates = await _streamProcessor
.AggregateStreamsAsync(period);
// Step 2: Calculate territory-level pool
var territoryPools = CalculateTerritoryPools(
streamAggregates,
await _revenueService
.GetTerritoryRevenueAsync(period));
// Step 3: Apply contractual terms per rights holder
var lineItems = new List<RoyaltyLineItem>();
foreach (var pool in territoryPools)
{
var contracts = await _contractService
.GetContractsAsync(pool.Territory);
foreach (var contract in contracts)
{
var holderStreams = streamAggregates
.Where(s =>
s.RightsHolderId == contract.HolderId
&& s.Territory == pool.Territory)
.Sum(s => s.StreamCount);
var grossAmount = holderStreams
* pool.PerStreamRate;
var holdback = grossAmount
* contract.HoldbackPercentage;
var netAmount = grossAmount - holdback;
lineItems.Add(new RoyaltyLineItem
{
RightsHolderId = contract.HolderId,
StreamCount = holderStreams,
ShareAmount = grossAmount,
HoldbackPct = contract.HoldbackPercentage,
NetAmount = netAmount,
PeriodStart = period.Start,
PeriodEnd = period.End,
Territory = pool.Territory
});
}
}
// Step 4: Aggregate and generate payments
var payments = AggregateToPayments(lineItems);
// Step 5: Persist and audit
await _repository.SaveAccrualsAsync(lineItems);
await _auditLog.LogCalculationAsync(
period, lineItems.Count);
return new RoyaltyReport
{
Period = period,
TotalStreams = streamAggregates
.Sum(s => s.StreamCount),
TotalPayout = payments.Sum(p => p.TotalAmount),
LineItemCount = lineItems.Count,
PaymentCount = payments.Count
};
}
}
Fraud Detection in Streaming
Artificial streaming (bot farms that generate fake plays to inflate royalty payments) costs the music industry hundreds of millions of dollars annually. Our fraud detection system uses multiple signals to identify suspicious streams: anomalous listening patterns (a single IP generating thousands of track plays), unusual completion rates (tracks played for exactly 30 seconds, the minimum threshold for a "qualified stream"), device fingerprint anomalies (thousands of different "devices" from a small IP range), and behavioral analysis (no skips, no saves, no playlist adds, just raw plays). Flagged streams are quarantined and excluded from royalty calculations pending investigation. The system uses an isolation forest algorithm for anomaly detection and maintains a blocklist of known botnet IP ranges. The fraud detection system processes the stream event stream in real-time using Apache Flink, maintaining a sliding 24-hour window of per-user, per-IP, and per-device statistics that feed into the anomaly detection model.
19. Content Ingestion Pipeline
The content ingestion pipeline is the gateway through which new music enters the platform. It receives audio files, metadata, and artwork from record labels and distributors (via DDEX, CD Baby, TuneCore, or direct API integration), validates the content, runs it through the encoding and enrichment pipeline, and publishes it to the catalog. The pipeline must handle 100,000+ new tracks per day, detect duplicates and potential copyright conflicts, and ensure that content is available for streaming within 1 hour of ingestion.
Ingestion Pipeline Architecture
Validation and Deduplication
Before any content is encoded, it passes through a multi-stage validation pipeline. The first stage checks file integrity (valid audio format, sample rate within expected range, no corruption). The second stage detects duplicates by computing an audio fingerprint (Chromaprint/AcoustID) and checking against the existing catalog. The third stage verifies rights, the distributor must provide a valid ISRC and prove they have the rights to distribute the content. The fourth stage checks content policy compliance (no hate speech, no non-consensual content, no copyright-infringing samples). Content that fails any stage is rejected with a detailed error report sent to the distributor.
C#
public class IngestionPipeline
{
private readonly IFileValidator _validator;
private readonly IFingerprintService _fingerprint;
private readonly IRightsVerifier _rights;
private readonly IContentPolicy _policy;
private readonly IEncodingPipeline _encoding;
public async Task<IngestionResult> IngestTrackAsync(
IncomingTrack track, DistributorInfo distributor)
{
var errors = new List<ValidationError>();
// Stage 1: File validation
var fileValid = await _validator
.ValidateAsync(track.AudioFile);
if (!fileValid.IsValid)
errors.AddRange(fileValid.Errors);
// Stage 2: Duplicate detection via audio fingerprint
var fingerprint = await _fingerprint
.ComputeFingerprintAsync(track.AudioFile);
var existingMatch = await _fingerprint
.FindMatchAsync(fingerprint);
if (existingMatch != null)
{
if (existingMatch.Isrc != track.Isrc)
errors.Add(new ValidationError(
"DUPLICATE_RECORDING",
$"Audio matches existing track " +
$"{existingMatch.TrackId} " +
$"(ISRC: {existingMatch.Isrc})"));
}
// Stage 3: Rights verification
var rightsValid = await _rights.VerifyAsync(
track.Isrc, distributor);
if (!rightsValid)
errors.Add(new ValidationError(
"RIGHTS_MISMATCH",
"Distributor lacks rights for this ISRC"));
// Stage 4: Content policy
var policyCheck = await _policy
.CheckAsync(track.AudioFile);
if (!policyCheck.Approved)
errors.AddRange(policyCheck.Violations);
if (errors.Any())
return IngestionResult.Rejected(errors);
// All checks passed, enqueue for encoding
var jobId = await _encoding.EnqueueAsync(track);
return IngestionResult.Accepted(jobId);
}
}
20. Audio Fingerprinting and Identification
Audio fingerprinting enables the system to identify tracks from short audio snippets (similar to Shazam) and detect duplicate content across the catalog. The fingerprinting system uses perceptual hashing algorithms that extract a compact representation of the audio's spectral characteristics, robust enough to survive re-encoding, compression, and environmental noise. The core algorithm (based on Chromaprint/AcoustID) produces a 32-bit hash every 0.37 seconds of audio, creating a "fingerprint" sequence that uniquely identifies the track.
Fingerprint Matching Architecture
C#
public class AudioFingerprintService
{
private readonly IFingerprintExtractor _extractor;
private readonly IFingerprintIndex _index;
public async Task<FingerprintMatchResult>
IdentifyTrackAsync(AudioStream unknownAudio)
{
// Extract fingerprints from the unknown audio
// (3-second snippet is sufficient)
var fingerprints = await _extractor
.ExtractFingerprintsAsync(
unknownAudio,
segmentDuration:
TimeSpan.FromSeconds(0.37));
// Search index for matching fingerprint
// subsequences using LSH for sub-millisecond lookup
var candidates = await _index
.SearchAsync(fingerprints, maxCandidates: 10);
if (!candidates.Any())
return FingerprintMatchResult.NoMatch();
// Score each candidate by alignment quality
var scored = candidates.Select(c => new
{
Candidate = c,
Score = ComputeAlignmentScore(
fingerprints, c.Fingerprints),
TimeOffset = ComputeTimeOffset(
fingerprints, c.Fingerprints)
})
.OrderByDescending(x => x.Score)
.First();
if (scored.Score < MinimumConfidenceThreshold)
return FingerprintMatchResult.NoMatch();
return FingerprintMatchResult.Identified(
scored.Candidate.TrackId,
scored.Candidate.TrackTitle,
confidence: scored.Score,
offsetMs: scored.TimeOffset);
}
}
public class FingerprintIndex
{
private readonly Dictionary<uint, List<FingerprintEntry>>
_lshBuckets;
public async Task<List<FingerprintEntry>> SearchAsync(
uint[] queryFingerprints, int maxCandidates)
{
var candidateScores =
new Dictionary<Guid, int>();
foreach (var fp in queryFingerprints)
{
// Find all tracks with a similar fingerprint
// (within Hamming distance threshold)
var similar = FindWithinHammingDistance(
fp, maxDistance: 3);
foreach (var entry in similar)
{
candidateScores.TryGetValue(
entry.TrackId, out int count);
candidateScores[entry.TrackId] = count + 1;
}
}
return candidateScores
.OrderByDescending(kv => kv.Value)
.Take(maxCandidates)
.Select(kv => GetEntry(kv.Key))
.ToList();
}
}
The fingerprint index is built from all 100 million tracks in the catalog, producing approximately 80 billion fingerprint entries (each track produces approximately 800 fingerprints at 0.37-second intervals for a 5-minute track). The LSH index compresses this into approximately 2 billion unique hash buckets, requiring about 50 GB of memory. This easily fits in a single server with 64 GB RAM. For redundancy, we replicate the index across 3 nodes with round-robin load balancing.
21. Live Audio Streaming
Live audio includes live radio stations, artist live sessions, podcasts going live, and social listening parties. Unlike on-demand content, live audio has no pre-encoded segments. Audio must be captured, encoded, and delivered with the lowest possible latency. The end-to-end latency from artist microphone to listener speaker must be under 3 seconds for interactive live sessions (where listeners can react and artists respond in real time) and under 30 seconds for broadcast-style live radio.
Live Audio Pipeline
Live Ingest Protocol
For artist live sessions, we use WebRTC for ultra-low-latency ingest (sub-100ms from artist to server). The artist's recording software (or browser) establishes a WebRTC peer connection to our live ingest server, which receives raw Opus-encoded audio at 48 kHz. The ingest server then transcodes the audio to AAC for HLS packaging, segments it into 1-second chunks, and pushes to the LL-HLS origin. For third-party radio stations and existing RTMP sources, we support RTMP ingest with on-the-fly transcoding.
C#
public class LiveStreamManager
{
private readonly IWebRtcIngest _webrtcIngest;
private readonly ILiveTranscoder _transcoder;
private readonly IManifestWriter _manifestWriter;
private readonly IChatService _chat;
public async Task<LiveStreamSession>
StartLiveSessionAsync(
Guid artistId, LiveStreamConfig config)
{
var sessionId = Guid.NewGuid();
// Configure live transcoder
var transcoderConfig = new LiveTranscoderConfig
{
InputCodec = "opus",
InputSampleRate = 48000,
OutputVariants = new[]
{
new OutputVariant("low", "aac", 64000),
new OutputVariant("mid", "aac", 128000),
new OutputVariant("high", "aac", 256000)
},
SegmentDuration =
TimeSpan.FromSeconds(1),
KeyframeInterval =
TimeSpan.FromSeconds(2)
};
// Start WebRTC ingest
var ingestSession = await _webrtcIngest
.StartSessionAsync(
sessionId, transcoderConfig);
// Start live transcoder
var transcoderSession = await _transcoder
.StartTranscodingAsync(
ingestSession, transcoderConfig);
// Start LL-HLS manifest writer
var manifest = await _manifestWriter
.StartLiveManifestAsync(
sessionId, transcoderConfig);
// Initialize live chat
await _chat.CreateRoomAsync(
sessionId, artistId);
return new LiveStreamSession
{
SessionId = sessionId,
IngestUrl = ingestSession.WebRTCUrl,
PlaybackUrl = manifest.MasterPlaylistUrl,
ChatRoomId = sessionId,
StartedAt = DateTime.UtcNow
};
}
}
22. Device Sync and Multi-Device
Modern users access music across multiple devices, including phone during commute, desktop at work, smart speaker at home, car on the road, and TV for background music. The system must seamlessly sync playback state across all devices, support handoff (start on phone, continue on speaker), and enforce concurrent stream limits (typically 1 device for free tier, unlimited for premium).
Playback State Sync
The playback state (current track, position, queue, shuffle mode, repeat mode) is maintained on the server and synchronized to all user devices in real time via WebSockets. When a user pauses on their phone, the desktop player shows the same paused state within 200ms. When a user adds a track to the queue on their laptop, the phone's queue updates immediately. The server is the authoritative source of truth for playback state. Clients send state change events (play, pause, seek, skip) to the server, which broadcasts the update to all other connected devices.
C#
public class PlaybackSyncService
{
private readonly IWebSocketManager _wsManager;
private readonly IDistributedCache _stateCache;
public async Task<PlaybackState> GetPlaybackStateAsync(
Guid userId)
{
var state = await _stateCache
.GetAsync<PlaybackState>(
$"playback:{userId}");
return state ?? PlaybackState.Empty(userId);
}
public async Task BroadcastStateChangeAsync(
Guid userId, PlaybackStateChange change)
{
// Update authoritative server state
var currentState =
await GetPlaybackStateAsync(userId);
var newState =
ApplyChange(currentState, change);
await _stateCache.SetAsync(
$"playback:{userId}", newState,
TimeSpan.FromHours(2));
// Broadcast to all connected devices
var devices = await _wsManager
.GetConnectedDevicesAsync(userId);
var updateMessage = new PlaybackUpdate
{
State = newState,
ChangedBy = change.DeviceId,
Timestamp = DateTime.UtcNow
};
foreach (var device in devices)
{
if (device.Id != change.DeviceId)
{
await _wsManager.SendToClientAsync(
device.ConnectionId, updateMessage);
}
}
}
public async Task<bool> CheckConcurrentStreamAsync(
Guid userId, Guid deviceId)
{
var activeStreams = await _stateCache
.GetAsync<List<ActiveStream>>(
$"streams:{userId}");
var otherActiveStreams = activeStreams?
.Where(s => s.DeviceId != deviceId
&& s.IsActive)
.ToList()
?? new List<ActiveStream>();
var subscription = await _subscriptionService
.GetSubscriptionAsync(userId);
if (subscription.Tier == SubscriptionTier.Free
&& otherActiveStreams.Any())
{
await StopStreamAsync(
otherActiveStreams.First().DeviceId);
return true;
}
return true;
}
}
Device Handoff
Device handoff enables users to seamlessly transfer playback from one device to another. For example, a user finishing their commute can tap a "Transfer to Home Speaker" button in the app, and the speaker immediately starts playing from where the phone left off. The handoff is implemented by sending a transfer command to the target device's WebSocket connection, including the track ID, position, and queue. The target device initiates a new stream and seeks to the transferred position within 2 seconds. The source device stops playback and shows a confirmation message. To implement this reliably, both devices must be connected to the WebSocket server simultaneously. The transfer protocol includes a brief overlap period where both devices decode audio, ensuring gapless transition with no silence.
23. Monitoring and Observability
Monitoring a music streaming service requires tracking metrics across multiple domains: streaming quality (buffer health, bitrate distribution, startup latency), infrastructure health (CPU, memory, network), CDN performance (cache hit ratio, latency, error rate), business metrics (daily active users, streams per user, churn), and ML model quality (recommendation accuracy, search relevance). A unified observability platform aggregates all these signals into actionable dashboards and automated alerting.
Key Metrics Dashboard
| Metric | Target | Alert Threshold | Measurement |
|---|---|---|---|
| Playback Startup Latency (P95) | Less than 200ms | Greater than 500ms for 5 minutes | Client-reported, aggregated via telemetry |
| Buffer Stall Rate | Less than 0.1% | Greater than 0.5% for 10 minutes | Client-side buffer underrun events |
| CDN Cache Hit Ratio | Greater than 95% | Less than 90% for 15 minutes | CDN provider metrics API |
| API Error Rate | Less than 0.01% | Greater than 0.1% for 5 minutes | API Gateway access logs |
| Search Latency (P99) | Less than 100ms | Greater than 200ms for 5 minutes | Application APM traces |
| Encoding Pipeline Lag | Less than 1 hour | Greater than 4 hours | Kafka consumer lag on encoding topic |
| DRM License Issuance Latency | Less than 50ms | Greater than 200ms for 5 minutes | License server metrics |
| Active Users (Real-Time) | N/A | Drop greater than 20% from baseline | Redis HyperLogLog counter |
Observability Stack
Our observability stack consists of Prometheus (metrics collection and alerting), Grafana (dashboards and visualization), Jaeger (distributed tracing), the ELK stack (centralized logging with Logstash, Elasticsearch, and Kibana), and PagerDuty (incident management and on-call rotation). Every service emits structured JSON logs with correlation IDs that enable tracing a single user request across all microservices. Custom exporters expose service-specific metrics (e.g., a CDN exporter that polls provider metrics APIs every 60 seconds).
24. Security, Compliance and Licensing
Security in music streaming encompasses content protection (DRM), user data privacy (GDPR, CCPA), payment security (PCI DSS for subscriptions), API security (rate limiting, abuse prevention), and music licensing compliance (territorial rights, usage reporting to PROs). Each domain has distinct requirements and regulatory frameworks.
Security Architecture
| Domain | Threat | Mitigation |
|---|---|---|
| Content Protection | Audio ripping, screen recording | Widevine L1/FairPlay DRM, watermarking |
| User Privacy | Data breach, unauthorized access | Encryption at rest (AES-256), field-level encryption for PII |
| API Security | DDoS, credential stuffing, scraping | Cloudflare DDoS, rate limiting, CAPTCHA |
| Payment | Card fraud, subscription abuse | Stripe/PCI DSS, device fingerprinting |
| Licensing | Unauthorized use, missing reports | Automated usage reporting to PROs, audit trails |
| Internal | Insider threats, data exfiltration | RBAC, audit logs, anomaly detection |
Watermarking for Forensic Tracking
Beyond DRM, we embed inaudible forensic watermarks into every audio stream. Each watermark encodes a unique identifier linking the stream to the specific user and session that played it. If audio is ripped through screen recording or other circumvention methods, the watermark can identify the source account. Watermarks are implemented using spread-spectrum techniques that survive re-encoding, compression, and common audio transformations. The watermark data is imperceptible to listeners (changes in amplitude below the threshold of human hearing, typically less than 0.1 dB) but robustly detectable with proprietary analysis tools.
Licensing Compliance
Music licensing involves multiple rights holders and complex royalty structures. For each territory, we must report usage data to Performing Rights Organizations (PROs) like ASCAP, BMI, SESAC (US), PRS (UK), GEMA (Germany), and SACEM (France). These reports include per-track play counts, timestamps, and listener territories. Our licensing compliance module automatically generates and submits these reports monthly, using standardized DDEX messaging formats. The module also enforces territorial restrictions. If a label has not granted streaming rights in a particular country, tracks from that label are excluded from the catalog in that territory.
C#
public class LicensingComplianceService
{
private readonly IProReportingClient _proClient;
private readonly ITerritoryRightsChecker _rightsChecker;
public async Task<ProReport>
GenerateMonthlyProReportAsync(
DateMonth period, string territory,
string proCode)
{
var streams = await _streamRepository
.GetStreamsAsync(period, territory);
var trackUsage = streams
.GroupBy(s => s.TrackId)
.Select(g => new TrackUsage
{
TrackId = g.Key,
Isrc = g.First().TrackIsrc,
PlayCount = g.Count(),
TotalListenSeconds = g
.Sum(s => s.ListenDurationSec),
UniqueListeners = g
.Select(s => s.UserId)
.Distinct().Count()
})
.ToList();
return new ProReport
{
Territory = territory,
Period = period,
ProCode = proCode,
TrackUsages = trackUsage,
GeneratedAt = DateTime.UtcNow
};
}
public async Task<bool>
CheckTerritoryAvailabilityAsync(
Guid trackId, string territory)
{
var rights = await _rightsChecker
.GetTerritoryRightsAsync(trackId);
return rights.Any(r =>
r.Territory == territory
&& r.IsActive
&& r.ValidUntil > DateTime.UtcNow);
}
}
25. Cost Estimation
Running a music streaming service at scale involves significant infrastructure costs, with CDN bandwidth being the single largest expense. The cost model must account for compute (encoding, API servers, ML inference), storage (audio files, metadata, analytics), bandwidth (CDN delivery, inter-service communication), and third-party services (DRM licensing, music metadata databases, lyrics providers).
Monthly Infrastructure Cost Breakdown
| Component | Specification | Monthly Cost |
|---|---|---|
| CDN Bandwidth (CloudFront) | 150 PB/month at $0.02/GB (tiered) | $3,000,000 |
| CDN Bandwidth (Fastly) | 30 PB/month backup at $0.08/GB | $2,400,000 |
| Origin Storage (S3) | 15 PB audio, metadata, and backups | $350,000 |
| Encoding Pipeline (GPU) | 200 GPU instances (p4d.24xlarge) | $1,200,000 |
| API Servers (EKS) | 500 c6g.2xlarge pods | $200,000 |
| Database (PostgreSQL RDS) | Multi-AZ, db.r6g.4xlarge times 6 | $85,000 |
| Redis Cluster (ElastiCache) | Node cache r6g.2xlarge times 20 | $55,000 |
| Elasticsearch (OpenSearch) | r6g.xlarge.search times 15 | $65,000 |
| Kafka (MSK) | kafka.m5.2xlarge times 12 | $48,000 |
| ClickHouse (Analytics) | Compute-optimized cluster | $95,000 |
| ML Training (SageMaker) | p4d.24xlarge times 8 (weekly) | $120,000 |
| ML Inference (SageMaker) | ml.g5.2xlarge times 10 | $60,000 |
| DRM Licensing (Widevine) | Per-device fees | $200,000 |
| Lyrics Licensing | Musixmatch + Genius API | $150,000 |
| Monitoring (Datadog/Grafana Cloud) | Enterprise tier | $80,000 |
| Other (DNS, Certificates, Tools) | Miscellaneous | $50,000 |
Total Monthly Cost Summary
| Category | Monthly Cost | Percentage of Total |
|---|---|---|
| CDN Bandwidth | $5,400,000 | 45% |
| Compute (Encoding + API + ML) | $1,580,000 | 13% |
| Storage (S3 + DB + Cache) | $490,000 | 4% |
| Licensing (DRM + Lyrics) | $350,000 | 3% |
| Third-Party Services | $130,000 | 1% |
| Total Infrastructure | $7,950,000 | 66% |
| Engineering Team (50 engineers) | $2,500,000 | 21% |
| Operations and Support | $500,000 | 4% |
| Total Operating Cost | $10,950,000 | 100% |
26. API Design
The music streaming API is organized into domain-specific microservices, each with a well-defined responsibility. The API Gateway handles authentication, rate limiting, and request routing, while individual services handle catalog operations, playback management, social interactions, and business logic. All APIs follow REST conventions with JSON payloads and use OAuth 2.0 JWT tokens for authentication.
Core API Endpoints
HTTP
# Playback API
GET /api/v1/player/playback-state
PUT /api/v1/player/play
PUT /api/v1/player/pause
PUT /api/v1/player/next
PUT /api/v1/player/previous
PUT /api/v1/player/seek?position_ms=30000
PUT /api/v1/player/volume?volume=80
PUT /api/v1/player/repeat?mode=track
PUT /api/v1/player/shuffle?enabled=true
POST /api/v1/player/transfer
POST /api/v1/player/add-to-queue
GET /api/v1/player/queue
# Catalog API
GET /api/v1/tracks/{id}
GET /api/v1/tracks/{id}/audio-variants
GET /api/v1/albums/{id}
GET /api/v1/albums/{id}/tracks
GET /api/v1/artists/{id}
GET /api/v1/artists/{id}/top-tracks
GET /api/v1/artists/{id}/related-artists
# Search API
GET /api/v1/search?q=bohemian&type=track,artist,album
GET /api/v1/search/autocomplete?q=bohe
GET /api/v1/search/semantic?q=sad+rainy
# Playlist API
GET /api/v1/playlists/{id}
POST /api/v1/playlists
PUT /api/v1/playlists/{id}
DELETE /api/v1/playlists/{id}
POST /api/v1/playlists/{id}/tracks
DELETE /api/v1/playlists/{id}/tracks/{pos}
PUT /api/v1/playlists/{id}/reorder
POST /api/v1/playlists/{id}/collaborators
# User API
GET /api/v1/users/me
GET /api/v1/users/me/top-artists
GET /api/v1/users/me/top-tracks
GET /api/v1/users/me/recently-played
GET /api/v1/users/me/saved-tracks
PUT /api/v1/users/me/saved-tracks/{id}
DELETE /api/v1/users/me/saved-tracks/{id}
# Lyrics API
GET /api/v1/tracks/{id}/lyrics
# Artist Dashboard API
GET /api/v1/artist-dashboard/streams
GET /api/v1/artist-dashboard/listeners
GET /api/v1/artist-dashboard/playlists
GET /api/v1/artist-dashboard/revenue
API Response Format
JSON
{
"data": {
"track_id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Bohemian Rhapsody",
"artists": [
{
"artist_id": "a1b2c3d4",
"name": "Queen",
"verified": true
}
],
"album": {
"album_id": "e5f6a7b8",
"title": "A Night at the Opera",
"release_date": "1975-10-31"
},
"duration_ms": 354320,
"explicit": false,
"preview_url": "https://preview.example.com/track123/preview.mp3",
"audio_variants": [
{ "tier": "low", "bitrate": 64000, "codec": "aac" },
{ "tier": "medium", "bitrate": 128000, "codec": "aac" },
{ "tier": "high", "bitrate": 160000, "codec": "aac" },
{ "tier": "very_high", "bitrate": 256000, "codec": "aac" },
{ "tier": "lossless", "bitrate": 800000, "codec": "flac" }
]
},
"metadata": {
"request_id": "req_abc123",
"latency_ms": 12
}
}
Streaming URL Generation
Audio streaming URLs are not returned directly from the catalog API. Instead, the client calls the playback API with a track ID, and the server generates a time-limited, signed URL for each audio segment. This URL includes a JWT token with an expiration time (typically 4 hours), the user's subscription tier (determining which quality levels are available), and a DRM key reference. The signed URL prevents hotlinking and ensures that only authenticated subscribers can access the audio content.
C#
public class PlaybackUrlGenerator
{
private readonly ISigningService _signer;
private readonly ISegmentStore _segments;
public async Task<HlsManifestUrl>
GenerateManifestUrlAsync(
Guid trackId, Guid userId,
SubscriptionTier tier)
{
var variants = await _segments
.GetAvailableVariantsAsync(trackId);
var allowed = FilterByTier(variants, tier);
var manifest = new HlsMasterPlaylist();
foreach (var variant in allowed)
{
var signedBase = await _signer
.GenerateSignedBaseUrlAsync(
trackId, variant.Tier,
userId, expiresIn: TimeSpan.FromHours(4));
manifest.AddVariant(
variant, $"{signedBase}/playlist.m3u8");
}
var manifestJson = manifest.ToM3u8();
var manifestKey = await _segments
.UploadManifestAsync(
trackId, manifestJson);
return new HlsManifestUrl
{
MasterPlaylistUrl =
$"{CdnBase}/{manifestKey}",
ExpiresAt =
DateTime.UtcNow.AddHours(4)
};
}
private AudioVariant[] FilterByTier(
AudioVariant[] variants,
SubscriptionTier tier)
{
return tier switch
{
SubscriptionTier.Free => variants
.Where(v => v.Bitrate <= 128_000)
.ToArray(),
SubscriptionTier.Standard => variants
.Where(v => v.Bitrate <= 256_000)
.ToArray(),
SubscriptionTier.Premium => variants,
_ => variants
};
}
}
Rate Limiting Strategy
The API Gateway enforces rate limits using a sliding window algorithm backed by Redis. Free tier users are limited to 100 requests per minute, standard tier to 500 requests per minute, and premium tier to 2,000 requests per minute. Playback-related APIs (streaming segments, playback state updates) have separate, higher rate limits because they are called frequently by the client (segment requests can exceed 30 per minute at 2-second segment intervals). Rate limit headers (X-RateLimit-Remaining, X-RateLimit-Reset) are included in every API response so clients can proactively throttle their requests.
27. Testing Strategy
Testing a music streaming service requires coverage across multiple dimensions: unit tests for business logic, integration tests for service interactions, load tests for performance validation, chaos tests for resilience verification, and end-to-end tests for user experience validation. Given the system's complexity and scale, a comprehensive testing strategy is essential to prevent regressions and ensure reliability.
Testing Pyramid
| Test Type | Count | Execution Time | Environment | Coverage Target |
|---|---|---|---|---|
| Unit Tests | 15,000+ | Under 5 minutes | CI pipeline (in-memory) | 90% code coverage |
| Integration Tests | 2,000+ | Under 15 minutes | Test containers (PostgreSQL, Redis) | All API endpoints |
| Contract Tests | 500+ | Under 5 minutes | Pact broker | All inter-service contracts |
| Load Tests | 50 scenarios | Under 30 minutes | Staging (production-equivalent) | P99 latency targets |
| Chaos Tests | 20 experiments | Under 60 minutes | Staging (dedicated cluster) | Failure recovery |
| E2E Tests | 200+ | Under 20 minutes | Staging (full stack) | Critical user journeys |
Load Testing Scenarios
Load tests validate that the system meets performance targets under expected and peak traffic conditions. We use k6 for load generation, running against a staging environment that mirrors production topology (same number of pods, same instance types, same database configuration). The following scenarios are tested nightly in our CI pipeline:
JavaScript
// k6 load test for concurrent audio streaming
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const startupLatency = new Trend('playback_startup');
const segmentLatency = new Trend('segment_download');
const stallRate = new Rate('buffer_stalls');
export const options = {
scenarios: {
// Normal evening peak
peak_traffic: {
executor: 'ramping-vus',
startVUs: 1000,
stages: [
{ duration: '2m', target: 50000 },
{ duration: '10m', target: 50000 },
{ duration: '2m', target: 1000 },
],
},
// Album release burst
album_release: {
executor: 'ramping-arrival-rate',
startRate: 100,
stages: [
{ duration: '30s', target: 5000 },
{ duration: '5m', target: 5000 },
{ duration: '2m', target: 200 },
],
},
},
thresholds: {
http_req_duration: ['p(95)<200', 'p(99)<500'],
playback_startup: ['p(95)<200'],
segment_download: ['p(95)<100'],
buffer_stalls: ['rate<0.001'],
},
};
export default function () {
// Simulate user session
const userId = authenticate();
// Browse catalog
searchCatalog('bohemian rhapsody');
// Start playback
const startTime = Date.now();
const manifest = getPlaybackManifest(userId, trackId);
startupLatency.add(Date.now() - startTime);
// Stream segments
for (let i = 0; i < 180; i++) {
const segStart = Date.now();
const response = getSegment(manifest.variants[2], i);
segmentLatency.add(Date.now() - segStart);
if (response.status !== 200) {
stallRate.add(1);
}
sleep(2); // 2-second segment duration
}
}
Chaos Engineering
We use Chaos Monkey and Litmus Chaos for automated failure injection. The following chaos experiments are run weekly on our staging environment: Pod kill (simulate random pod termination), Network partition (simulate split-brain between service regions), Database failover (trigger PostgreSQL primary switch), Redis eviction (simulate cache miss storm), CDN origin failure (simulate S3 outage), and Kafka broker failure (simulate message queue degradation). Each experiment has a defined blast radius (maximum number of affected pods), a rollback condition (automatic abort if error rate exceeds threshold), and a success criterion (system recovers within the defined SLA).
C#
[Fact]
public async Task AudioPlayback_SurvivesRedisOutage()
{
// Arrange: Start playback on a user
var userId = await CreateTestUser();
var trackId = await CreateTestTrack();
var playback = await StartPlayback(userId, trackId);
// Act: Simulate Redis outage
await ChaosEngine.InjectAsync(new PodChaos
{
Selector = new LabelSelector("app=redis-cache"),
Mode = "all",
Duration = TimeSpan.FromMinutes(5)
});
// Assert: Playback continues (falls back to DB)
await Task.Delay(TimeSpan.FromSeconds(30));
var state = await GetPlaybackState(userId);
Assert.Equal(PlaybackStatus.Playing, state.Status);
Assert.Equal(trackId, state.CurrentTrackId);
Assert.True(state.PositionMs > 0,
"Playback position should advance during outage");
// Assert: Cache warms back up after recovery
await Task.Delay(TimeSpan.FromMinutes(6));
var metrics = await GetCacheMetricsAsync();
Assert.True(metrics.HitRatio > 0.80,
"Cache hit ratio should recover after outage");
}
28. Interview Q&A Deep Dive
System Design Interview Questions
Q: How do you handle the thundering herd problem when a popular album drops at midnight?
A: The thundering herd problem manifests as millions of users simultaneously requesting the same audio segments, overwhelming both the CDN and origin infrastructure. We address this with a multi-layered strategy. First, CDN cache warming pre-pushes all encoded segments to every edge location 30 minutes before release. Second, the CDN origin shield deduplicates concurrent origin requests for the same segment. Third, the client-side player uses a staggered startup (random jitter of 0 to 2 seconds before requesting the first segment) to spread the initial load. Fourth, the API rate limiter caps per-user request rates to prevent any single client from overwhelming the system. The combination of these four techniques reduces the peak load by approximately 95% compared to a naive implementation.
Q: What happens when a user's network drops from WiFi to cellular mid-stream?
A: The adaptive bitrate controller detects the bandwidth change within 1 to 2 segment requests (2 to 4 seconds). When the measured throughput drops below the current tier's bitrate, the controller immediately switches to the next lower tier. The buffer safety mechanism ensures that there are always at least 5 seconds of audio buffered, providing a smooth transition. The HLS manifest contains all quality variants, so the client can switch without any server interaction. In practice, users rarely notice the quality change because the audio quality difference between adjacent tiers (e.g., 160 kbps to 128 kbps) is subtle to most listeners. If the network drops to very low bandwidth (below 64 kbps), the client uses the HE-AAC v2 codec variant at 24 kbps, which provides acceptable voice quality at extreme compression.
Q: How do you ensure exactly-once playback counting for royalty purposes?
A: Strict exactly-once delivery is prohibitively expensive in a distributed system. Instead, we use at-least-once delivery with idempotent processing. The client reports playback events with a unique event ID. The server maintains a deduplication table (Redis bloom filter backed by PostgreSQL) that tracks processed event IDs. If a duplicate event arrives (due to client retry or network retransmission), it is silently dropped. For the critical "track completed" event (which triggers royalty accrual), the client sends the event multiple times (at 50%, 75%, and 100% completion) and the server processes only the first occurrence. The deduplication window is 24 hours, after which stale events are rejected. This approach provides effectively-once semantics with minimal infrastructure overhead.
Q: How do you design the recommendation engine to avoid creating filter bubbles?
A: Filter bubbles occur when the recommendation algorithm only shows users content similar to what they have already consumed, narrowing their musical taste over time. We combat this with a multi-armed bandit approach that balances exploitation (showing tracks similar to the user's known preferences) with exploration (introducing new genres, artists, and moods). Specifically, 15% of recommendation slots are reserved for "exploration" content that is deliberately dissimilar to the user's history. The exploration rate is personalized based on user behavior: users with high skip rates on familiar content get more exploration, while users with consistent listening patterns get less. Additionally, editorial curation provides a human counterbalance to algorithmic recommendations, ensuring that culturally significant music and emerging artists receive exposure regardless of algorithmic signals.
Q: How would you design the system to support spatial audio (Dolby Atmos) playback?
A: Spatial audio adds a new dimension to the encoding and streaming pipeline. Dolby Atmos music uses an object-based audio format where individual instruments and vocals are positioned in 3D space. The encoding pipeline adds a new pass that generates Atmos master files from the ADM (Audio Definition Model) files provided by the label. The Atmos stream is delivered as a separate audio object in the HLS manifest, alongside the traditional stereo mix. The client detects Atmos support (hardware DAC capability, headphones, Atmos-enabled speakers) and automatically requests the spatial stream. The Atmos stream requires approximately 3 to 5 Mbps, roughly 15 to 25 times the stereo bitrate, making it feasible only for WiFi and high-bandwidth cellular connections. The adaptive bitrate controller treats Atmos as the highest quality tier and gracefully falls back to stereo when bandwidth is insufficient.
Q: How do you handle music licensing across different territories?
A: Music licensing is a patchwork of territorial rights. A track might be available in the US but not in Japan due to licensing restrictions. We model this as a many-to-many relationship between tracks and territories, stored in a PostgreSQL table with a composite index on (track_id, territory). When a user requests a track, the system checks their detected territory against the track's available markets before generating the streaming manifest. The territory check is cached in Redis for 1 hour to avoid repeated database lookups. When rights change (e.g., a license expires or a new territory is added), the catalog change event triggers a cache invalidation. The system supports both whitelist (explicitly list available territories) and blacklist (explicitly list excluded territories) models, as different labels use different conventions.
Q: How do you detect and prevent artificial streaming (bot farms)?
A: We use a multi-layered fraud detection approach. The first layer is rule-based: flags streams from IPs with abnormally high play counts (more than 100 unique tracks per hour from a single IP), streams with suspiciously uniform completion rates (exactly 30 seconds, the royalty threshold), and streams from known VPN/proxy exit nodes. The second layer is ML-based: an isolation forest model trained on historical legitimate vs. fraudulent stream features (IP reputation, device fingerprint diversity, behavioral patterns). The third layer is network analysis: we build a graph of IP addresses, devices, and user accounts to detect coordinated inauthentic behavior (e.g., 1,000 accounts streaming the same obscure track from the same IP range). Flagged streams are quarantined and excluded from royalty calculations. Repeat offenders are permanently banned, and their associated rights holders are investigated for potential collusion.
Key Numbers to Remember
| Metric | Value |
|---|---|
| Playback startup latency target | Less than 200ms |
| Segment duration | 2 seconds (on-demand), 1 second (live) |
| CDN cache hit ratio target | Greater than 95% |
| Catalog size | 100M+ tracks, growing 100K/day |
| Search latency (P99) | Less than 100ms |
| Concurrent streams (peak) | 50 million |
| Monthly CDN transfer | approximately 194 petabytes |
| Loudness normalization target | -14 LUFS |
| DRM license validity | 30 days (renewed automatically) |
| Monthly infrastructure cost | approximately $7.95M |
| Encoding speed | 2x real-time per quality tier |
| Fingerprint index size | approximately 50 GB (in-memory) |
| Recommendation latency | Less than 10ms (ANN lookup) |
| Offline device limit | 5 devices per account |
| Collaborative playlist limit | Optimistic concurrency (version vectors) |
Pre-Interview Checklist
- Understand audio codecs (AAC, Ogg Vorbis, Opus, FLAC) and their tradeoffs
- Know how HLS and DASH adaptive bitrate streaming works
- Design a multi-CDN architecture with intelligent routing
- Understand DRM systems (Widevine, FairPlay) and offline playback
- Know the music ingestion pipeline (DDEX, validation, encoding, publication)
- Discuss recommendation engine architecture (two-tower, collaborative filtering, content-based)
- Understand royalty calculation (pro-rata, territorial rates, fraud detection)
- Know search architecture (Elasticsearch, autocomplete, semantic search with embeddings)
- Explain the playlist system (collaborative editing, algorithmic generation, concurrency)
- Understand live audio streaming (WebRTC ingest, LL-HLS delivery, synchronization)
- Discuss monitoring strategy (RED/USE metrics, distributed tracing, alerting)
- Know security and compliance (DRM, watermarking, GDPR, PRO reporting)
- Be able to estimate infrastructure costs at scale
- Understand device sync and multi-device playback state management
13. Social Features
Social features transform music streaming from a solitary listening experience into a shared community activity. They drive engagement, increase session duration, and create network effects that improve retention. Our social layer supports following artists and friends, activity feeds, track sharing, collaborative playlists, and real-time social listening sessions (also known as listening parties).
Social Graph Model
The social graph is stored as a directed graph in PostgreSQL (follow relationships) with a Redis-backed adjacency list cache for fast lookups. The graph supports millions of users with thousands of connections each. We avoid a dedicated graph database (Neo4j, Amazon Neptune) because our social operations are simple (follow, unfollow, get followers, get following) and do not require complex graph traversal queries. PostgreSQL handles these operations efficiently with B-tree indexes on the (follower_id, followee_id) composite key.
Activity Feed
The activity feed shows what friends are listening to, shared tracks, new playlists, and artist activity. It uses a fan-out-on-write pattern: when a user performs a social action (shares a track, creates a public playlist), the event is written to that user's activity log and simultaneously fanned out to all followers' feed caches in Redis. For users with many followers (artists with millions of followers), we use a fan-out-on-read hybrid approach: the feed is assembled at read time by merging the user's own activity with a "popular items" stream from artists they follow. This avoids the write amplification problem where a single post from a popular artist would require millions of cache updates.
Social Listening Sessions
Social listening sessions (similar to Spotify's "Group Session" or Discord's "Listen Along") allow multiple users to listen to the same audio in synchronized real-time. The session host controls playback (play, pause, skip, queue), and all participants hear the same audio at the same time. This is implemented using WebSockets for real-time state synchronization and server-side playback state tracking. The server sends playback commands (play track X at position Y) to all connected clients simultaneously. Clients compensate for network jitter using local buffer management and periodic clock synchronization with the server.
The synchronization protocol works by having the server maintain a canonical playback timeline. When the host issues a command, the server broadcasts the command with a timestamped directive to all clients. Each client applies the command and adjusts its local playback position to match the server's timeline. To handle network latency differences between clients, the server sends each client a personalized offset that accounts for their specific round-trip time to the server. This ensures that all clients are within approximately 200ms of each other, which is imperceptible for audio listening.