Design Spotify: A Production System Architecture That Handles 500M Users
Design Spotify's real-world system architecture for 500M users.
20+ years shipping large-scale distributed systems. Written from production experience, not tutorials.
- ✓Solid grasp of fundamentals
- ✓Comfortable reading code examples
- ✓Basic production concepts
Spotify's architecture uses microservices with gRPC for internal communication, Cassandra for playlist storage, and a custom content delivery network for audio streaming. They rely on event-driven pipelines for recommendations and offline processing for analytics.
Think of Spotify as a massive jukebox with a librarian who remembers every song you've ever played. The jukebox (CDN) stores the music files, the librarian (backend) keeps your playlists and history, and a recommendation engine (data pipeline) suggests new songs based on what you and others like. The trick is making all this work instantly for millions of people at once.
You're building a music streaming service. Day one, it works fine. Day 100, your database is on fire because you stored playlists in a relational database with joins. I've seen this exact pattern kill a startup's launch. The problem isn't the music — it's the metadata. Playlists, user history, social features — these are graph-like, write-heavy, and need to be available globally. Spotify handles 500M users with 100M+ tracks. They don't use a single database. They use a carefully chosen set of tools, each optimized for a specific job. After this article, you'll be able to design a system that scales to millions of users without falling over.
Why Microservices? The Monolith That Couldn't Stream
Spotify started as a monolith. By 2012, it was a nightmare. Deploying a change to the recommendation engine required redeploying the entire backend. A bug in playlist sharing could take down search. They split into microservices — each owning a bounded context: playlist service, social graph service, audio delivery service, search service, etc. This let them scale independently. The playlist service, for example, is write-heavy and uses Cassandra. The search service is read-heavy and uses Elasticsearch. They communicate via gRPC for low latency and Kafka for async events. The trade-off? Distributed debugging is harder. You need tracing (Jaeger) and structured logging. But for a system with 500M users, the isolation is worth it.
Cassandra for Playlists: Why Not SQL?
Playlists are write-heavy. Users add/remove tracks, reorder, share. A relational database with normalized tables (playlists, playlist_tracks, users) would require joins on every read. With millions of playlists and billions of tracks, joins become a bottleneck. Cassandra is a wide-column store that excels at write throughput and can handle large datasets with no single point of failure. The data model: playlist_tracks table with partition key playlist_id and clustering columns (position, track_id). This allows fast range queries for a playlist's tracks. The downside? No joins, no transactions across partitions. You have to denormalize. For example, store playlist metadata (name, owner) in a separate table with the same partition key. Spotify also uses Cassandra for user library (saved tracks, albums) and listening history. The trade-off is eventual consistency — but for playlists, that's acceptable. If a user adds a track and it doesn't appear immediately on another device, it's fine.
Audio Streaming: The CDN and Adaptive Bitrate
Streaming audio is the core of Spotify. They don't serve audio from their own servers — that would be insane. They use a CDN (Google Cloud CDN, Akamai, etc.) to cache audio files close to users. Audio files are encoded in multiple bitrates (96kbps, 160kbps, 320kbps Ogg Vorbis). The client selects the appropriate bitrate based on network conditions (adaptive bitrate streaming). The challenge: how does the client know which CDN edge to hit? Spotify uses a 'delivery service' that returns a signed URL to the closest CDN edge. The URL includes a token for authentication. The client then fetches the audio file directly from the CDN. This offloads traffic from Spotify's infrastructure. The CDN handles the heavy lifting of global distribution. The trade-off: cost. CDN egress is expensive. Spotify optimizes by caching popular tracks aggressively and using peer-to-peer for some scenarios (though that's less common now).
Recommendations: The Offline Pipeline That Never Sleeps
Spotify's recommendation engine is the secret sauce. It's not real-time — it's a batch processing pipeline that runs periodically (daily or hourly). It uses collaborative filtering, natural language processing (on song lyrics, blog posts), and audio analysis (tempo, key, loudness). The pipeline is built on Apache Beam (or Scio, a Scala wrapper for Beam) running on Google Cloud Dataflow. It processes user listening history (stored in Cassandra) and track features (stored in Bigtable) to generate recommendations. The output is stored in a 'recommendation table' in Cassandra, keyed by user_id. When a user opens the app, the client fetches recommendations from this table. The trade-off: recommendations are not real-time. If a user listens to a new genre, it won't affect recommendations until the next pipeline run. But that's acceptable — users don't expect instant personalization.
Search: Elasticsearch at Scale
Search is critical. Users search for tracks, albums, artists, playlists. Spotify uses Elasticsearch with a custom scoring function that combines text relevance (BM25) with popularity signals. The index is sharded across multiple nodes. Updates come from Kafka events (when a new track is added, metadata changes). The challenge: keeping the index fresh. Spotify uses near-real-time indexing with a refresh interval of 1 second. For large updates (e.g., new album), they use bulk indexing. The trade-off: Elasticsearch is memory-hungry. Each shard consumes heap. They optimize by using doc-values for aggregations and avoiding heavy analyzers on large fields.
Social Features: Graph Database for Friends and Follows
Spotify has social features: follow friends, see their playlists, collaborative playlists. This is a graph problem. They use a graph database (Neo4j) for the social graph. Why not relational? Because queries like 'find all friends of friends who listened to this track' are expensive in SQL with recursive joins. Neo4j handles these with ease. The social graph is write-heavy (follow/unfollow) but read-heavy for recommendations. They replicate the graph to Cassandra for read-heavy workloads (denormalized). The trade-off: maintaining consistency between Neo4j and Cassandra. They use an event-driven approach: when a follow happens, write to Neo4j, then publish event to Kafka, which updates Cassandra. This is eventually consistent, but acceptable.
Event-Driven Architecture with Kafka
Kafka is the backbone of Spotify's async communication. Every significant action (play track, add to playlist, follow user) publishes an event to Kafka. Multiple consumers process these events: search indexing, recommendation pipeline, analytics, billing (for premium). This decouples services and allows independent scaling. The key design: use Avro for schema evolution. Spotify has a schema registry to ensure compatibility. They also use Kafka Streams for real-time processing (e.g., updating user session state). The trade-off: Kafka adds latency (milliseconds) and operational complexity. But for a system with hundreds of microservices, it's essential.
Caching: Redis for Hot Data
Spotify uses Redis extensively for caching: user sessions, recently played tracks, top charts, and playlist metadata for popular playlists. The key is to cache only hot data. For example, the top 1% of playlists account for 80% of reads. They cache these in Redis with a TTL of 5 minutes. For less popular playlists, they fall through to Cassandra. They also use Redis for rate limiting and distributed locks. The trade-off: cache invalidation is hard. They use a write-through cache: when a playlist is updated, they invalidate the Redis key and let the next read repopulate it from Cassandra. This ensures consistency at the cost of a cache miss.
Monitoring and Observability
With hundreds of services, you need centralized logging, metrics, and tracing. Spotify uses Google Cloud Monitoring for metrics, Jaeger for distributed tracing, and ELK stack for logs. Every service exposes Prometheus metrics (request rate, latency, error rate). They have dashboards for each service and SLOs (e.g., playlist read latency p99 < 100ms). Tracing is critical for debugging latency issues across services. They use a correlation ID propagated via gRPC metadata. The trade-off: instrumentation adds overhead. They sample traces (1% of requests) to keep costs down. But when debugging an incident, they can increase sampling rate dynamically.
The Playlist That Broke Cassandra
- Never let a single partition grow unbounded.
- Always estimate max partition size and use bucketing or time-based partitioning.
nodetool cfstats playlist_tracks | grep 'Partition size'nodetool tablestats playlist_tracks| File | Command / Code | Purpose |
|---|---|---|
| CassandraPlaylistSchema.systemdesign | CREATE TABLE IF NOT EXISTS playlist_tracks ( | Cassandra for Playlists |
| ElasticsearchMapping.systemdesign | PUT /tracks | Search |
| SocialGraphNeo4j.systemdesign | MATCH (u:User {id: $user_id})-[:FOLLOWS]->(friend:User)-[:LISTENED]->(track:Trac... | Social Features |
| KafkaEventSchema.systemdesign | { | Event-Driven Architecture with Kafka |
| RedisCachingPlaylist.systemdesign | function getPlaylistTracks(playlistId): | Caching |
| TracingSetup.systemdesign | "go.opentelemetry.io/otel" | Monitoring and Observability |
Key takeaways
Interview Questions on This Topic
How does Spotify handle playlist consistency across devices when a user adds a track on mobile and then opens the desktop app?
Frequently Asked Questions
20+ years shipping large-scale distributed systems. Written from production experience, not tutorials.
That's Real World. Mark it forged?
4 min read · try the examples if you haven't