Caso 4 · Music streaming

Spotify: catálogo, playback y recomendaciones

Streaming de audio bajo demanda con búsqueda, playlists, ranking y respeto a licencias por región.

Recursos

Track, Album, Artist, Playlist, User, ListenEvent.

Operaciones

Catálogo, búsqueda, playback, playlists, recomendaciones.

Escala

Cientos de millones de usuarios, decenas de millones de tracks.

Riesgo

Licencias por región, cold start, tiempo de primer byte.

Requisitos

Lo primero que aclaras: el alcance funcional y los SLOs no negociables.

Funcionales

  • Reproducir tracks bajo demanda con scrub/seek.
  • Búsqueda de tracks, artistas, álbumes, podcasts.
  • Playlists propias y colaborativas.
  • Recomendaciones personalizadas (Discover Weekly).
  • Modo offline con descargas cifradas.

No funcionales

  • Time-to-first-byte < 200 ms global.
  • 99.95% disponibilidad de playback.
  • Geo-fencing por licencias regionales.
  • Skips/seeks instantáneos con prefetch.
  • DRM / token de stream con expiración corta.

Cómo aclarar requisitos en la entrevista

Casos de uso típicos. Free vs Premium tier (con/sin ads), modo offline (descargas cifradas), Family plan compartido, podcasts y audiolibros, lyrics sincronizadas, social (lo que escuchan tus amigos), Discover Weekly / Daily Mix, audio en TV / smart speakers / car play, gapless playback.

Preguntas que debes hacer:

• ¿Music + podcasts + audiolibros o solo music?
• ¿Free con ads y Premium, o un solo tier?
• ¿Soporte offline con cifrado y expiración? Cuántos devices simultáneos.
• ¿Recomendaciones dentro de este alcance o se asume otro sistema?
• ¿Multi-device sincronizado (continuar donde dejaste)?
• ¿Social features: ver lo que escuchan tus amigos?
• ¿Karaoke / lyrics sincronizadas?
• ¿Royalties a labels: en este alcance o aparte?
• ¿Hi-Fi (FLAC) además de OGG/AAC?

Frase clave: "Asumo music streaming, free + premium tiers, sin podcasts ni offline. Recomendaciones las trato como caja negra. Royalties los menciono pero no los detallo."

Estimaciones de capacidad

Resoluciones año/día/seg para defender search engine, CDN y tier dedicado.

600M
MAU
~250M premium · ~350M ad-supported.
~150M DAU · ~5M concurrentes en peak.
100M
Tracks
+ ~5M podcasts + audiolibros.
~60K nuevos tracks/día (~22M/año).
~5B/día
Listen events
~1.8T/año · ~60K/s avg.
~150K/s en peak global tarde.
~50K QPS
Search
~4.3B/día · ~1.6T/año.
Pico tarde-noche por timezone.
~2 PB
Audio storage
~100M tracks × ~3 bitrates × 5 MB.
+~3 PB/año por crecimiento de catálogo.
96–320 kbps
Bitrates
Adaptive según red y tier.
Hi-Fi 1411 kbps para tier premium plus.
15 min
TTL stream token
~150M tokens generados/día.
Refresh silencioso antes de expirar.
99.95%
SLO playback
~4.4 h/año · ~21 min/mes.
~43 s/día permitidos.

Endpoints

MétodoPathDescripciónNotas
GET/api/tracks/{trackId}Metadata del track.Cacheable global.
GET/api/tracks/{trackId}/streamURL firmada al CDN según región.Auth requerida.
GET/api/search?q=...&type=track,artist,albumBúsqueda federada.Search index (Elastic/Solr).
POST/api/playlistsCrear playlist.
POST/api/playlists/{id}/tracksAñadir tracks a playlist.
GET/api/recommendations?context=homeMix personalizado.Reco service.
POST/api/events/listenEventos de reproducción.Stream analítico.

Cómo defiendo estas APIs en la entrevista

Recursos plurales, sub-recursos para acciones. /tracks/{id} es metadata (idempotente, cacheable globalmente); /tracks/{id}/stream es signing endpoint que devuelve manifest URL firmada. Las playlists son recurso con sub-recurso /tracks para añadir/quitar.

Tokens cortos para licencias. El stream URL incluye un token con expiración 15 min y geo claim. Si el user cancela mid-month, el token actual expira solo. Cliente refresca silenciosamente antes de expirar. 403 si la región no tiene licencia para ese track.

Search con filtros, no recursos separados. GET /search?q=...&type=track,artist,album en vez de tres endpoints — una sola query a Elasticsearch. Pagination con cursor opaco. GET /recommendations es lectura idempotente, sticky por user para A/B consistency.

Frase clave: "Catalog reads son cacheables globalmente. Streaming reads requieren signed URL con geo + expiración. Listen events son fire-and-forget al stream. Cada endpoint encaja en un patrón consistente."

Ejemplo: streaming

GET /api/tracks/t_223/stream
Authorization: Bearer ...

200 OK
{
  "trackId": "t_223",
  "manifestUrl": "https://cdn.spot/eu-w/t_223.m3u8?sig=...",
  "bitrates": [96, 160, 320],
  "expiresAt": "2026-05-01T14:00:00Z",
  "drmToken": "..."
}

Arquitectura

flowchart LR Client(["App / Web / Speaker"]) --> GW GW{{"🌐 API GATEWAY
Auth + Region routing"}} --> Cat["Catalog Service"] Cat --> CatDB GW --> Search["Search Service"] Search --> SIdx GW --> Reco["Recommendation Service"] Reco --> FS Reco --> Embed GW --> Stream["Stream Service
token signer"] Stream --> CDN(["CDN POPs"]) CDN -->|miss| AudioStore Client -. listen events .-> Pipe subgraph CatDB["💾 Catalog DB · Postgres / Cassandra"] direction TB c1["tracks"] c2["albums"] c3["artists"] c4["playlists"] c5["users"] end subgraph SIdx["🔍 Search Index · Elasticsearch"] direction TB e1["tracks_index"] e2["artists_index"] e3["albums_index"] end subgraph FS["💾 Feature Store · Bigtable / Redis"] direction TB f1["user:id"] f2["item:id"] f3["cohort:id"] end subgraph Embed["🧠 Embeddings ANN · FAISS / ScaNN"] direction TB v1["track_vectors"] v2["user_vectors"] end subgraph AudioStore["🗄️ Object Storage · S3 / GCS"] direction TB a1["audio/96kbps"] a2["audio/160kbps"] a3["audio/320kbps"] end subgraph Pipe["📨 Kafka · analytics + reco loop"] direction TB p1["listen_events"] end classDef gateway fill:#fbbf24,stroke:#f59e0b,stroke-width:3px,color:#0f172a,font-weight:bold classDef edge fill:#1e1b4b,stroke:#a78bfa,stroke-width:2px,color:#e0e7ff classDef service fill:#064e3b,stroke:#34d399,stroke-width:2px,color:#d1fae5 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class GW gateway class CDN edge class Cat,Search,Reco,Stream service class Client client

Cómo lo explico en la entrevista

Búsqueda. El usuario teclea "Bad Bunny". Cliente hace GET /api/search?q=.... El gateway routea al Search Service que pega a Elasticsearch con un query multi-match (track + artist + album), filtra por la región del user (licencias) y devuelve top resultados con score. La búsqueda nunca toca la DB relacional principal, justamente porque LIKE '%q%' no escala.

Playback. Tap en un track. Cliente hace GET /api/tracks/{id}/stream. El Stream Service: (1) verifica que licensed_regions incluya la región del user, (2) firma una URL al CDN POP regional con expiración 15 min, (3) devuelve el manifest con bitrates disponibles. El cliente se conecta al CDN y baja segmentos de audio. El backend nunca sirve bytes.

Recomendaciones. Mientras escucha, el cliente envía eventos al Kafka listen_events. El stream alimenta dos cosas: (1) el Feature Store online para recomendaciones (Discover Weekly, daily mix), y (2) el pipeline de royalties para licencias por play. Las recos online se sirven leyendo features de user + items candidatos (vía ANN sobre embeddings) y rankeando.

Frase clave: separo catálogo, búsqueda, recomendaciones y streaming en servicios distintos. El audio nunca toca mi app — vive en object storage detrás de CDN, con manifiesto firmado por región para respetar licencias.

Por qué cada componente

Catalog Service + DBTracks, álbumes y artistas en relacional con joins. Lecturas masivas, escrituras esporádicas.
Search Service + ElasticsearchFull-text con sinónimos, fuzzy, ranking. SQL no aguanta a esa escala.
Stream Service + signed URLsGeo-fencing por región, expiración corta, revoca acceso si user cancela suscripción.
Object Storage + CDNAudio escala horizontal sin egress propio. POP regional para latencia < 200 ms.
Reco Service + Feature StoreModelo offline + serving online consistente. Same code path en train y serve.
Embeddings ANNVecinos cercanos a "lo que ya escuchaste". Sub-10 ms en millones de tracks.
Kafka listen_eventsUna fuente para reco, royalties, trending y BI. Reprocesable.
DRM token corto15 min de validez con renew silencioso. Si user cancela mid-month, expira solo.

Cuellos de botella

  • Cold cache para tracks nuevos virales — primer impacto pega al origen.
  • Search index lag detrás de releases (si no es streaming-friendly).
  • Token refresh storm si miles renuevan al mismo tiempo.
  • Cross-region latency en países sin POP cercano.
  • Feature store hot keys en tracks populares globalmente.

Mejoras / cómo escala

  • Pre-cache regional según predicción de releases populares.
  • Bulk signing + renew silencioso antes de expiración.
  • Predictive prefetch del siguiente track de la queue (gapless playback).
  • Edge compute para personalización de queue sin round-trip al backend.
  • Replicación de features hot a múltiples regiones.
  • Indexación incremental en search engine para releases en tiempo casi real.

Por qué estos servicios cloud (vs alternativas)

Elasticsearch / OpenSearchsearch index

Por qué: full-text con synonyms, fuzzy matching, BM25 ranking, custom analyzers por idioma, geo filters por región. Manejas 50K QPS de búsqueda con response time <100 ms.

vs Solr: equivalente, ES tiene mejor stack y cloud managed. vs Algolia: excelente DX pero costo prohibitivo a esta escala. vs Postgres FTS: queda corto a 50K QPS y ranking inferior.

Catalog DB (Postgres / Cassandra)tracks/albums

Por qué: joins entre tracks, albums, artists, ISRC unique. Cassandra si quieres write-heavy multi-region; Postgres si lectures dominan y cabe en uno.

vs DynamoDB: sin joins, replicarías metadata en muchos lugares. vs MongoDB: aceptable pero performance de joins con $lookup es pobre.

Object Storage + CDN regionalaudio delivery

Por qué: S3/GCS guarda los segmentos de audio en múltiples bitrates; CDN los entrega desde POP regional con manifest firmado por licencia. Geo-fencing en el signing evita reproducir tracks no licenciados en un país.

vs streaming server propio: imposible a 60K listen events/s globales. vs servir desde la DB: destruirías ancho de banda y latencia.

FAISS / ScaNN / HNSWembeddings ANN

Por qué: vecinos cercanos sobre embeddings de ~100M tracks en sub-10 ms. Recall >95% con HNSW configurado bien. Crítico para Discover Weekly y "you may also like".

vs Postgres + pgvector: aceptable para <1M items, no escala a 100M. vs Elasticsearch dense_vector: funciona pero menos óptimo en recall/latencia. vs brute force kNN: impensable a esta escala.

Apache Kafkalisten_events stream

Por qué: partitioning por user_id mantiene orden por usuario. Múltiples consumers (royalties, analytics, recommendations, fraud) sobre el mismo stream. Replay para corregir bugs en pipelines downstream.

vs Pub/Sub: aceptable, menos control de retention. vs Kinesis: AWS-only, cuotas más rígidas. vs SQS: queue, no permite múltiples consumers independientes.

Feature Store (Bigtable / DynamoDB / Redis)recos online

Por qué: latencia <10 ms en bulk MGET de features de user e items para servir recos. Bigtable/Dynamo para volumen, Redis para hot features.

vs Postgres: latencia mayor, no escalable a 12K QPS de bulk reads. vs solo cache: sin persistencia se pierde el feature store.

Diseño de base de datos

Catálogo en DB relacional, search engine dedicado, eventos en stream para reco loop.

tracks
track_idUUID PK
album_idUUID FK
titleTEXT
duration_msINT
isrcVARCHAR(12)
licensed_regionsTEXT[]
idx (album_id), idx (isrc UNIQUE)
playlists
playlist_idUUID PK
owner_idUUID FK
nameTEXT
collaborativeBOOLEAN
tracksJSONB[ord]
idx (owner_id, updated_at)
artists / albums
artist_id / album_idUUID PK
name / titleTEXT
genresTEXT[]
release_dateDATE
idx GIN (genres) para filtros
listen_events (stream)
user_idUUID
track_idUUID
ms_playedINT
contextJSONB
tsTIMESTAMPTZ
Tópico Kafka particionado por user_id
Sink: feature store + analytics

Cómo defiendo este diseño de base de datos

Catálogo en relacional, audio en object storage, search en engine dedicado. Cada uno por una razón: catálogo necesita joins (track → album → artist), audio son blobs (no caben en DB), search requiere full-text con synonyms y BM25. Mezclarlos sería ineficiente en tres dimensiones.

Licencias como first-class data. El campo licensed_regions en tracks es array de country codes — la query de stream verifica antes de firmar. Esto evita que un usuario en X país acceda a contenido no licenciado, requisito legal duro.

Playlists como JSONB con orden. En lugar de tabla N:M con position column, guardo tracks como JSONB array — preserva orden, edición es write-once, lecturas son single row. Si edits granulares fueran frecuentes, tabla N:M con index sería mejor; en Spotify la mayoría son pocos edits.

Listen events particionados por user_id. Esto mantiene orden de sesión (importante para reco y skipping rate), permite rebuild por usuario, y distribuye carga entre brokers. Sink dual: feature store (rápido, hot) y warehouse (lento, frío).

Frase clave: "El schema reconoce que tracks/albums son data fría con muchas lecturas, listens son data caliente con muchas escrituras, y playlists son data tibia con writes esporádicos. Cada una tiene su storage."

Decisiones técnicas

TemaDecisiónPor qué
CatálogoServicio aparte con DB read-heavy y cache.Las lecturas son brutales, la metadata es global.
StreamingAudio en object storage + CDN, manifiesto firmado.Latency baja y control de licencias por región.
BúsquedaSearch engine dedicado con sinónimos y ranking.SQL no aguanta consultas full-text a esa escala.
PlaylistsDB con relación N:M y caché por usuario.Lecturas frecuentes, escrituras esporádicas.
RecomendacionesModelo offline + servicio online con feature store.Tiempo de respuesta predecible y A/B testing.
LicenciasGeo-fencing en el firmado del manifiesto.Bloquear contenido no licenciado por región.

Cómo defiendo estas decisiones técnicas

Constraint fundamental: licencias por región. No es solo un detalle — define la arquitectura. CDN regional con signed manifest geo-fenced, base de datos con licensed_regions, royalty pipeline alimentado por listen events. Si fallo aquí, hay demanda de Universal Music.

Recomendaciones offline + serving online. Discover Weekly se entrena offline con todo el corpus de listens; el modelo se deploya a serving online que sirve recos en sub-100ms via feature store. Train/serve consistency es el camino — ambos usan el mismo código de transformación de features.

Métricas que validan. Time-to-first-byte (target < 200 ms global). Skip rate (alto = recos malos). Cache hit ratio CDN (afecta egress cost directamente). Token refresh storm rate (si bursts, agrego jitter en client). License compliance audit (legal-grade).

Cuándo cambio de opinión. Hi-Fi mainstream → más bitrates, más storage, más bandwidth. Live concerts → ingest server con LL-HLS. Podcasts dominantes → reconsider catalog DB schema (los podcasts son episodios de show, modelo distinto).

Frase clave: "Cada decisión está atada a un constraint duro: licencias (legal), latencia (UX), costo egress (negocio). El sistema no es elegante, es una composición justificada de tradeoffs."

Pitfalls

Streaming sin CDN

Saturarías egress y latencia se dispara fuera de tu región.

Búsqueda en SQL

LIKE %q% no escala. Search engine dedicado, sin excusas.

Recomendaciones síncronas

Si entrenas en el path crítico, p99 explota. Mantén modelo offline.

Script de respuesta: “Separo catálogo, búsqueda, playlists y recomendaciones en servicios distintos. El audio nunca toca mi app: vive en object storage y se entrega por CDN, con manifiesto firmado por región para respetar licencias. La búsqueda usa un search engine dedicado, las recomendaciones se calculan offline y se sirven online desde un feature store, y los eventos de listen alimentan el pipeline analítico para mejorar el ranking.”

Tiers de almacenamiento

El catálogo es relativamente pequeño (~2 PB) pero el listen log crece eternamente. Tiering apropiado define el costo a largo plazo.

Hot · CDN + RAM
Top tracks (long-tail invertido), DRM tokens, search index calientes, feature store online.
CDN regional + Redis + Elasticsearch hot tier
~$0.02–0.08/GB egress · ~$36/GB RAM
Warm · object storage Standard
Catálogo completo de audio (todos los bitrates), metadata DB, playlists.
S3 Standard + Postgres / Cassandra
~$0.023/GB/mes audio
Cold · object storage IA
Tracks sin plays > 90 días, podcasts viejos, listen events archivados (analytics warehouse).
S3 Standard-IA / GCS Nearline
~$0.0125/GB/mes
Frozen · archive
Royalty audit logs > 7 años (legal), tracks delistados pero retenidos por contrato.
S3 Glacier Deep Archive
~$0.001/GB/mes · retrieval 12 h

Cómo defenderlo en la entrevista

Long tail invertido en audio. A diferencia de YouTube, en Spotify todos los tracks deben estar disponibles bajo demanda — un usuario buscando un track raro espera respuesta inmediata. Por eso el catálogo permanece en Standard, no en IA. Lo que sí va a IA: listen events archivados, royalty calculations históricos, tracks delistados.

Royalty pipeline obligatorio en archive. Por contrato con labels, debes mantener listen events anonimizados > 7 años para auditorías de royalties. Glacier Deep Archive a $0.001/GB/mes vs $0.023/GB/mes en Standard salva millones al año a esa escala.

Frase clave: "Audio caliente vive en CDN porque latency, catálogo en Standard porque acceso bajo demanda, listen events en IA porque agregaciones diarias, audit logs en Glacier porque legal-retention pero rara consulta."

Versión alternativa: con Load Balancer (no API Gateway)

Spotify tiene público (Web API, partners) y privado (apps oficiales). LB+Gateway juntos es lo común en producción.

flowchart LR Client(["App / Web / Speaker / Car"]) --> LB LB{{"⚖️ L7 LOAD BALANCER
NGINX / GCLB
TLS + region routing"}} --> Pod["API Pods
+ JWT/OAuth middleware
+ Rate limit"] Pod --> Cat["Catalog Service"] Cat --> CatDB Pod --> Search["Search Service"] Search --> SIdx Pod --> Reco["Reco Service"] Reco --> FS Pod --> Stream["Stream Service
token signer"] Stream --> CDN(["CDN POPs"]) CDN -->|miss| AudioStore Client -. listen events .-> Pipe subgraph CatDB["💾 Catalog DB"] direction TB c1["tracks"] c2["albums"] c3["artists"] c4["playlists"] end subgraph SIdx["🔍 Elasticsearch"] direction TB e1["tracks_index"] e2["artists_index"] end subgraph FS["💾 Feature Store"] direction TB f1["user:id"] f2["item:id"] end subgraph AudioStore["🗄️ Object Storage"] direction TB a1["audio/96"] a2["audio/160"] a3["audio/320"] end subgraph Pipe["📨 Kafka"] direction TB p1["listen_events"] end classDef lb fill:#34d399,stroke:#10b981,stroke-width:3px,color:#0f172a,font-weight:bold classDef edge fill:#1e1b4b,stroke:#a78bfa,stroke-width:2px,color:#e0e7ff classDef service fill:#064e3b,stroke:#34d399,stroke-width:2px,color:#d1fae5 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class LB lb class CDN edge class Pod,Cat,Search,Reco,Stream service class Client client

API Gateway vs Load Balancer: cómo defender cada uno

Spotify usa los dos. Web API pública con quotas a developers (Spotify for Developers) → Gateway. Tráfico de las apps oficiales y smart speakers → LB. La apps móvil hace ~150 requests por sesión; pasar todo por Gateway sería costoso y agregaría latencia inútil.

Frase clave: "Para Web API pública uso Gateway con OAuth y rate limits por client_id. Para tráfico de apps oficiales uso LB porque el auth es propio (JWT firmado por Spotify), las rate limits las hago en pod, y la latencia la cuido para skip/seek instantáneos."

Cuándo elegir API Gateway

  • Spotify for Developers Web API con OAuth y quotas por client_id.
  • Partner integrations (smart speakers, integradores).
  • API monetization con plans por tier.
  • Centralizar OAuth flows con scopes granulares.
  • Centralizar versioning (/v1/, deprecation Sunset headers).

Cuándo elegir Load Balancer

  • Apps oficiales (móvil, web, desktop, speaker) con auth propio.
  • Latency-sensitive: skip/seek en < 50 ms requiere round-trip mínimo.
  • Listen events ingest: 60K/s avg requiere endpoint barato.
  • Service mesh con sidecar para observability.
  • Stream Service firma URLs y devuelve manifest — pure compute, no necesita features de gateway.