Advanced
Streaming & Media
35 min

Design a Video Streaming Platform (Netflix)

A video streaming platform must ingest large video files, transcode them into dozens of resolution/codec combinations, distribute the encoded segments to a globally distributed CDN, and serve them to clients that adapt quality in real-time based on available bandwidth. Beyond delivery, it needs a content catalog, user profiles, watch history, and a recommendation engine — all at the scale of hundreds of millions of concurrent streams.

CDNHLSTranscodingAdaptive BitrateMicroservicesKafka

Design it yourself

Don't just read it — drag components onto a canvas and get Aria's interviewer review.

Requirements

Functional

  • Content creators (studios) upload raw video files for processing
  • Users can browse a content catalog (title, genre, cast, synopsis, thumbnail)
  • Users can play a video and have it adapt quality based on their bandwidth
  • Resume playback from where the user left off across devices
  • Search the catalog by title, genre, or cast
  • Personalised recommendations on the home screen
  • User profiles and parental controls per account

Non-Functional

  • 220M subscribers, peak 15M concurrent streams
  • 15% of global internet traffic at peak (Sandvine 2023)
  • Video start time < 2s (play button to first frame)
  • 99.99% streaming availability — buffering is user-visible
  • Support 4K HDR playback where network allows
  • Video content stored and replicated globally across CDN PoPs
  • Upload pipeline: raw video processed within 24h of ingestion

Capacity Estimation

Concurrent streams (peak)15M streams
Avg bitrate per stream~5 Mbps (mix of SD/HD/4K)
Total peak bandwidth15M × 5 Mbps = 75 Tbps
Catalog size~15,000 titles; each transcoded into ~50 files
Storage per title (all resolutions)~100 GB average
Total catalog storage15,000 × 100 GB = 1.5 PB
CDN storage (global replication)~10× = 15 PB across CDN PoPs
Daily watch hours220M users × 2h avg = 440M hours/day

High-Level Components

Studio Upload Portal

Web interface for studios to upload raw video files. Uses chunked, resumable multipart uploads directly to object storage (S3). Publishes an UploadCompleted event to Kafka on success.

Transcoding Pipeline

Distributed pipeline of worker nodes that takes the raw video and produces multiple renditions (resolutions × codecs × HDR variants). Each rendition is segmented into small HLS/DASH chunks. Workers are horizontally scalable and GPU-accelerated.

Content Delivery Network (CDN)

Netflix Open Connect Appliances (OCA) — dedicated CDN servers co-located in ISP networks worldwide. During off-peak hours, OCAs pre-fetch popular content from origin. At streaming time, the client is directed to the nearest OCA for sub-millisecond-latency segment fetches.

Content Catalog Service

Stores metadata about each title: synopsis, cast, genres, maturity rating, available languages, thumbnail URLs, and manifest URLs. Backed by a relational DB (read replicas) and cached aggressively in Redis. Powers both the browse API and the search index.

Playback Service

Issues a signed playback token after validating the user's subscription, device limit, and DRM entitlement. Returns the CDN URL of the adaptive manifest (M3U8/MPD) for the requested title. The client then fetches segments directly from CDN.

ABR Streaming Client

The video player (HLS.js on web, ExoPlayer on Android, AVPlayer on iOS) downloads the manifest and then selects the appropriate quality rendition per ~4-second segment based on measured download throughput, buffer occupancy, and a buffer health target.

Watch History & Resume Service

Receives playback progress events (position every 10s) from clients. Stored in a fast key-value store (DynamoDB) keyed by (userId, titleId). On resume, returns the last known position. Also feeds the recommendation engine.

Recommendation Engine

Processes watch history, ratings, and catalogue metadata to generate personalised title rankings per user. Uses collaborative filtering (matrix factorisation) and content-based signals. Pre-computes recommendations daily; results stored per user in Redis for fast home-screen loads.

Search Service (Elasticsearch)

Full-text index over the content catalog. Supports fuzzy matching on title, cast names, and genre tags. Updated asynchronously when the catalog is modified. Returns title IDs ranked by relevance score.

Architecture Diagram

Rendering diagram…

Deep Dives

Video Transcoding Pipeline

Raw video from studios can arrive as ProRes 4K, 24fps, 300 GB files. The transcoding pipeline must produce: - Multiple resolutions: 240p, 360p, 480p, 720p, 1080p, 4K - Multiple codecs: H.264 (broad compatibility), H.265/HEVC (better compression), AV1 (best compression, slower encode) - HDR variants: SDR, HDR10, Dolby Vision where applicable - Audio tracks: stereo, 5.1, Dolby Atmos - Subtitles/captions: parsed and stored as WebVTT

A single title may produce 50+ output files, each segmented into 4-second HLS chunks.

Pipeline architecture: 1. Split: The raw video is split into 10-minute chunks (called "scenes") to enable parallel processing 2. Encode workers: Each chunk is dispatched to a GPU worker via a work queue (SQS/Kafka). Workers are stateless and auto-scale with the queue depth 3. Stitch: Encoded chunks are stitched back per rendition into a continuous segment sequence 4. Manifest generation: A final step generates the HLS master playlist (M3U8) referencing all renditions and segment URLs 5. Quality validation: Automated perceptual quality scoring (VMAF) rejects segments below threshold and triggers re-encode 6. Storage: Segments uploaded to origin S3; manifest metadata stored in the Catalog Service

Chunked parallel encoding is the critical optimisation — a 2-hour film that would take 8 hours to encode sequentially can be encoded in under 30 minutes by parallelising 12 × 10-minute chunks across 12 worker nodes.

Java — Transcoding orchestrator and GPU encode worker

// Transcoding job dispatcher — splits video and enqueues chunk jobs
public class TranscodingOrchestrator {

    private static final int CHUNK_DURATION_SECONDS = 600; // 10 minutes

    private final VideoSplitter splitter;
    private final SqsTemplate sqs;
    private final TranscodingJobRepository jobRepo;

    public void dispatch(UploadCompletedEvent event) {
        String rawS3Key = event.s3Key();
        long durationSecs = event.durationSeconds();
        long titleId = event.titleId();

        // Split into 10-minute chunks
        List<VideoChunk> chunks = splitter.split(rawS3Key, CHUNK_DURATION_SECONDS);

        List<String> targetRenditions = List.of(
            "240p-h264", "360p-h264", "480p-h264",
            "720p-h264", "1080p-h264", "1080p-hevc",
            "2160p-hevc", "2160p-av1"
        );

        TranscodingJob job = jobRepo.create(titleId, chunks.size() * targetRenditions.size());

        for (VideoChunk chunk : chunks) {
            for (String rendition : targetRenditions) {
                EncodeTask task = new EncodeTask(
                    job.id(), titleId, chunk.s3Key(),
                    chunk.index(), rendition
                );
                sqs.send("encode-tasks", task);
            }
        }
    }
}

// GPU worker — receives a single chunk encode task
@SqsListener("encode-tasks")
public void encodeChunk(EncodeTask task) {
    // Download chunk from S3
    Path inputPath = s3.download(task.chunkS3Key());

    // Run ffmpeg with target rendition settings
    RenditionConfig config = RenditionConfig.forProfile(task.rendition());
    Path outputPath = ffmpeg.encode(inputPath, config);

    // Upload encoded chunk to S3
    String outputKey = buildOutputKey(task);
    s3.upload(outputKey, outputPath);

    // Notify orchestrator of completion
    jobRepo.markChunkDone(task.jobId(), task.chunkIndex(), task.rendition(), outputKey);
}

Adaptive Bitrate (ABR) Streaming

ABR allows the video player to dynamically switch quality levels per segment based on real-time network conditions, ensuring uninterrupted playback at the best available quality.

HLS structure: ``` master.m3u8 (master playlist — lists all renditions) ├── 1080p.m3u8 (rendition playlist — lists segment URLs for 1080p) ├── 720p.m3u8 └── 360p.m3u8

1080p/ ├── seg000.ts (4-second MPEG-TS segment) ├── seg001.ts └── ... ```

ABR algorithm (simplified Buffer-Based Control): 1. Measure download speed of the last segment 2. Check current buffer occupancy (seconds of video buffered ahead) 3. If buffer > 30s: step up quality 4. If buffer < 10s: step down quality 5. If buffer < 5s (near-stall): drop to lowest quality immediately

Netflix's BOLA algorithm (Buffer Occupancy based Lyapunov Algorithm) optimises for maximum average bitrate subject to a buffer underflow probability constraint — it uses Lyapunov optimisation to make the quality-selection decision mathematically optimal given the buffer state.

Startup optimisation: - First segment always downloaded at lowest quality for fast start - Player estimates bandwidth from the first-segment download time - Switches to appropriate quality from segment 2 onward - Typical result: < 2s from play button to first frame

DRM integration: - Each segment is encrypted with AES-128 or CBCS (Common Encryption) - Decryption keys are issued by the Playback Service after entitlement check - Keys are bound to a device-specific certificate (Widevine on Android, FairPlay on iOS, PlayReady on Windows)

Java — Simplified buffer-based ABR quality selector

// Simplified ABR quality selector (runs in the client player)
public class AbrQualitySelector {

    // Bitrate ladder (bps) — matches server renditions
    private static final int[] BITRATE_LADDER = {
        300_000,    // 240p
        800_000,    // 360p
        1_500_000,  // 480p
        3_000_000,  // 720p
        6_000_000,  // 1080p
        15_000_000, // 4K
    };

    private static final double BUFFER_MAX     = 30.0; // seconds
    private static final double BUFFER_MIN     = 10.0;
    private static final double BUFFER_CRITICAL = 5.0;

    private int currentIndex = 0;

    /**
     * @param bufferSeconds seconds of video buffered ahead
     * @param bandwidthBps  estimated download bandwidth (bits/sec)
     * @return index into BITRATE_LADDER for the next segment
     */
    public int selectQuality(double bufferSeconds, long bandwidthBps) {
        if (bufferSeconds < BUFFER_CRITICAL) {
            // Critical: drop to lowest immediately
            currentIndex = 0;
        } else if (bufferSeconds < BUFFER_MIN) {
            // Low buffer: step down one level
            currentIndex = Math.max(0, currentIndex - 1);
        } else if (bufferSeconds > BUFFER_MAX) {
            // Healthy buffer: step up if bandwidth supports it
            int next = currentIndex + 1;
            if (next < BITRATE_LADDER.length
                    && bandwidthBps > BITRATE_LADDER[next] * 1.2) { // 20% headroom
                currentIndex = next;
            }
        }
        // else: buffer in stable range — hold current quality
        return currentIndex;
    }
}

CDN Strategy and Open Connect

Netflix operates its own CDN called Open Connect. Unlike third-party CDNs (Akamai, CloudFront), Netflix co-locates dedicated appliances (OCAs — Open Connect Appliances) inside ISP data centres, often at internet exchange points.

Why a proprietary CDN? - 15% of global internet traffic is overwhelmingly concentrated on Netflix content - Co-location inside ISPs eliminates transit costs (ISPs carry Netflix traffic for free or at minimal cost in exchange for appliances) - Netflix can control cache fill behaviour — pre-fetch popular content during off-peak hours (2-6am local) before any user requests it

Cache fill strategy: 1. Proactive fill (off-peak): Netflix's control plane identifies the top-N most-watched titles in each region based on viewing history. OCAs receive fill jobs to download segments for these titles from origin during off-peak hours. 2. Reactive fill (miss): If a requested segment is not cached, the OCA fetches it from origin and caches it. Subsequent requests are served locally. 3. Popularity signal: Real-time view counts feed a "title heat" score used to prioritise cache slots on capacity-limited appliances.

Client routing: - When the client calls the Playback Service, it receives a manifest URL pointing to a specific OCA hostname (e.g. `ipv4-c001-syd001.1.oca.nflxvideo.net`) - The Playback Service selects the optimal OCA based on the client's ISP, geography, current OCA load, and whether the content is already cached there - This steering decision is made in milliseconds using a real-time OCA health and inventory feed

Cache hit rate: Netflix targets > 95% CDN cache hit rate for popular content. Long-tail content (old titles, rare languages) may fall back to origin more often, but this represents a small fraction of total traffic.

Java — Playback Service: OCA selection and signed manifest URL

// Playback Service — OCA selection and manifest URL generation
@Service
public class PlaybackService {

    private final OcaSteeringService ocaSteering;
    private final EntitlementService entitlement;
    private final ManifestStore manifestStore;
    private final SigningKeyService signingKeys;

    public PlaybackSession startPlay(long userId, long titleId, String clientIp,
                                     String deviceId) {
        // 1. Validate entitlement (subscription active, concurrent stream limit)
        entitlement.validate(userId, deviceId);

        // 2. Select best OCA for this client
        OcaNode bestOca = ocaSteering.selectOca(clientIp, titleId);

        // 3. Fetch manifest metadata from catalog
        ManifestMetadata meta = manifestStore.getManifest(titleId);

        // 4. Build signed manifest URL pointing to selected OCA
        String manifestUrl = buildSignedUrl(bestOca.hostname(), meta.masterManifestPath(),
            userId, titleId, signingKeys.currentKey());

        // 5. Issue playback token (DRM key delivery auth)
        String playbackToken = signingKeys.issuePlaybackToken(userId, titleId,
            deviceId, Duration.ofHours(8));

        return new PlaybackSession(manifestUrl, playbackToken,
            meta.availableAudioTracks(), meta.availableSubtitles());
    }

    private String buildSignedUrl(String ocaHost, String path,
                                   long userId, long titleId, SigningKey key) {
        long expiresAt = Instant.now().plus(8, ChronoUnit.HOURS).getEpochSecond();
        String policy = String.format("uid=%d&tid=%d&exp=%d", userId, titleId, expiresAt);
        String signature = key.sign(policy);
        return "https://" + ocaHost + path + "?" + policy + "&sig=" + signature;
    }
}

Content Catalog Database Design

The content catalog must support: 1. Browse by genre, maturity rating, and language 2. Detail page load (title metadata, cast, available seasons/episodes) 3. Search by title, cast, and tags 4. Recommendation engine training (batch reads over the full catalog) 5. Personalisation signals (user-specific ratings, "continue watching")

Core tables (PostgreSQL): ```sql titles (title_id PK, type, release_year, maturity_rating, ...) localisations (title_id FK, locale, display_title, synopsis, ...) genres (genre_id PK, name) title_genres (title_id FK, genre_id FK) cast_members (person_id PK, name, ...) title_cast (title_id FK, person_id FK, role, billing_order) episodes (episode_id PK, title_id FK, season, episode_num, ...) manifests (manifest_id PK, title_id FK, resolution, codec, s3_key, ...) ```

Access patterns and caching: - Browse API: pre-computed genre shelves cached in Redis per locale, refreshed every 15 minutes - Detail page: title + cast + episodes cached in Redis for 5 minutes per title_id - Search: Elasticsearch index fed by a Kafka CDC stream from the titles table (Debezium) - Catalog updates (new title added): Kafka event triggers cache invalidation and Elasticsearch re-index

Recommendation pre-computation: - Nightly Spark job reads the full watch_history table and title feature vectors - Matrix factorisation (ALS) produces per-user recommendation scores - Top-50 title IDs per user written to Redis (`recs:{userId}` → list of title IDs) - Home screen API reads from Redis; never runs the ML model at request time

Java — Catalog Service with cache-aside and Kafka-triggered invalidation

// Content Catalog Service — title detail with cache-aside
@Service
public class CatalogService {

    private static final Duration CACHE_TTL = Duration.ofMinutes(5);
    private final RedisTemplate<String, TitleDetail> redis;
    private final TitleRepository titleRepo;
    private final EpisodeRepository episodeRepo;

    public TitleDetail getTitleDetail(long titleId, String locale) {
        String cacheKey = "title:" + titleId + ":" + locale;
        TitleDetail cached = redis.opsForValue().get(cacheKey);
        if (cached != null) return cached;

        // DB fetch — joined query for title + cast + episodes
        Title title = titleRepo.findByIdWithCast(titleId)
            .orElseThrow(() -> new NotFoundException("title", titleId));

        Localisation loc = title.getLocalisation(locale)
            .orElse(title.getLocalisation("en-US").orElseThrow());

        List<Episode> episodes = title.isSeriesType()
            ? episodeRepo.findByTitleIdOrderedBySeasonAndEpisode(titleId)
            : Collections.emptyList();

        TitleDetail detail = TitleDetail.builder()
            .titleId(titleId)
            .displayTitle(loc.displayTitle())
            .synopsis(loc.synopsis())
            .maturityRating(title.maturityRating())
            .releaseYear(title.releaseYear())
            .genres(title.genres())
            .cast(title.castMembers())
            .episodes(episodes)
            .thumbnailUrl(cdnUrl(title.thumbnailS3Key()))
            .build();

        redis.opsForValue().set(cacheKey, detail, CACHE_TTL);
        return detail;
    }

    // Called when catalog is updated — invalidate stale caches
    @KafkaListener(topics = "catalog-updates")
    public void onCatalogUpdate(CatalogUpdatedEvent event) {
        // Invalidate all locale variants for the updated title
        Set<String> keys = redis.keys("title:" + event.titleId() + ":*");
        if (keys != null && !keys.isEmpty()) redis.delete(keys);
    }
}

Key Trade-offs

Third-party CDN vs proprietary CDN (Open Connect)

Proprietary CDN (Open Connect)

Netflix's traffic volume makes transit costs prohibitive via commercial CDNs. Co-locating appliances inside ISPs eliminates transit, allows proactive cache fill during off-peak, and gives Netflix direct control over OCA steering decisions.

HLS vs DASH for adaptive streaming

Both — HLS for Apple devices, DASH elsewhere

Safari and iOS require HLS due to Apple's platform restrictions. DASH is an open standard with better tooling on Android and Smart TVs. Netflix produces both manifests from the same segment files, keeping storage overhead minimal.

Single transcode job vs parallel chunk encoding

Parallel chunk encoding

A 2-hour film encoded sequentially can take 8+ hours. Splitting into 10-minute chunks processed in parallel across GPU workers reduces this to under 30 minutes — essential for meeting the 24h SLA from upload to availability.

Real-time vs batch recommendation generation

Batch (nightly) with real-time watch signal ingestion

Running a matrix factorisation model at request time for 220M users is computationally infeasible. Nightly batch produces per-user recommendation lists. Real-time signals (current session watch history) adjust ordering at serve time with lightweight rules.

PostgreSQL vs NoSQL for content catalog

PostgreSQL (relational)

Catalog data is highly relational (titles, seasons, episodes, cast, genres). Complex join queries are needed for the editorial team's CMS. PostgreSQL with read replicas and a Redis cache layer handles the catalog read load comfortably without NoSQL complexity.

Interview Tips

  • 1Interviewers expect you to know the transcoding pipeline. Parallelise by splitting the video into chunks — this is the key insight that unlocks the 24h SLA.
  • 2ABR streaming is a standard follow-up. Know the HLS manifest structure (master playlist → rendition playlist → segment files) and explain why the client — not the server — selects quality.
  • 3Open Connect / CDN co-location is a differentiator answer. Most candidates say "use CloudFront." Knowing WHY Netflix built its own CDN (transit cost, proactive fill) signals senior-level depth.
  • 4Separate the upload pipeline from the streaming path clearly. Studios upload to origin; end users stream from CDN. The origin is never on the hot path for a streamed title.
  • 5For recommendations, avoid over-engineering. State the problem (collaborative filtering), name the algorithm (ALS matrix factorisation), describe the batch pre-computation, and explain that results are cached. That's enough for most interviews.
  • 6DRM is a common extension. Know: content encrypted with AES; decryption keys served by a licence server after entitlement check; keys bound to device certificate (Widevine / FairPlay).
  • 7When asked about scaling, tie numbers to your components. 15M concurrent streams × 5Mbps = 75 Tbps total bandwidth — this is why a CDN with 1000s of PoPs is mandatory; no single data centre can serve this.

Discussion

Discussion

Sign in to join the discussion.

Loading discussion…