Scope the requirements
The mental switch: this is not YouTube. It's a small, curated catalogue watched by hundreds of millions. The upload pipeline is offline and doesn't matter for the interview. The play pipeline is everything.
Functional — what it must do
- Browse a personalised home page (rows of titles); search.
- Play any title with adaptive bitrate; resume from position; multi-device.
- Concurrent-stream limits per account; per-profile watch history.
- Recommendations (offline model, served at browse time).
- Explicitly cut: uploads, live streaming, ad break insertion — offer them back.
Non-functional — the numbers that shape the design
- Startup latency under 2 s to first frame.
- ~250M subscribers, ~50M concurrent streams at peak.
- Bitrate: 5-15 Mbps sustained per stream, 4K adaptive.
- 99% of bytes served from ISP-embedded caches, not central origins.
- 99.99% availability on play; browse can degrade to a static list.
Everything hard about this problem I move to before someone hits play. Encoding, chunking, geographic placement, personalisation — all offline. The play button then hits a CDN and reads pre-computed rows.
Back-of-envelope estimates
The bandwidth number is astonishing and drives every decision. It's also why a normal CDN doesn't cut it — you build your own, deeper into the network.
| Quantity | Estimate | How you got there |
|---|---|---|
| Peak bandwidth | ~200 Tbps | 50M streams × ~4 Mbps average. Real peak, comparable to a large fraction of internet traffic. |
| Catalogue size | ~15,000 titles | small — the shocking number. Every title is high-value and can be re-encoded per format. |
| Encoded storage | ~10 PB | each title = ~150 encodings (codec × resolution × bitrate ladder); one 2-hour movie in ABR ladders ≈ ~20 GB. |
| OCA (edge box) count | ~15,000 boxes | Open Connect Appliances in ISP colos — 100+ TB each, pre-loaded with the most popular ~90% of watch time. |
| Origin fetches | ~1% of bytes | cold catalogue tail. The rest never touches an AWS-region origin. |
| Metadata QPS | ~200k/s peak | home page rows, resume points, entitlement checks — all handled by microservices, tiny compared to media. |
One in a hundred bytes served travels through the internet backbone. The rest are pulled from a box sitting in the customer's ISP building — that's the whole architectural bet.
Sketch the API
Two API surfaces: metadata (rows, search, playback URL) is REST; media is HTTP range requests to CDN edge boxes.
- Playback separates control from data: your API mints a signed manifest URL, and the client then fetches media directly from an OCA. Auth doesn't cost bandwidth.
- The manifest is a small text file (HLS m3u8 / DASH mpd). It's the pivot point where personalisation, geo-routing and CDN selection all show up.
Data model
Two databases with very different shapes: a small titles catalogue (mostly-read) and a large per-user history log (write-heavy).
Titles fits in Postgres and can be replicated everywhere for the browse path. Watch history is Cassandra: append-only, partition by profile_id, cluster by ts. Resume points are a Redis cache computed from history — cheap to rebuild after a bug, hot on every home-page render.
High-level design
Three planes: metadata (small, chatty), media (huge, one-way, cached deep in ISPs), and personalisation (offline models, online serving). The interview is drawing the seams.
- Home load: TV → API → home service → Redis returns the pre-computed row list for this profile (built overnight by the recs pipeline). Never touches Cassandra.
- Play tap: TV → API → playback service checks entitlement in Postgres, picks the nearest OCA for the client's IP, signs a manifest URL, returns it.
- TV requests the manifest, then fetches segments over HTTP direct to the OCA. If a segment isn't cached, OCA pulls from a regional cache; last resort is origin (S3).
- Client posts a beacon every ~10 s with position/bytes/device — history service appends to Cassandra, updates a resume point in Redis.
- Overnight, the recs pipeline reads Cassandra history, runs models offline, writes personalised rows to Redis for tomorrow's home page. And the OCA-fill service pushes tomorrow's new-releases to every OCA before anyone plays them.
Deep dives — where the interview is won
Adaptive bitrate: give the client the choice and get out of the way
Every title is pre-encoded into a ladder — say 240p at 300 kbps, 480p at 700 kbps, up to 4K at 15 Mbps — chunked into ~2-6 s segments. The client's player watches its download rate against segment size and picks the highest bitrate it can sustain; if the buffer drops, it steps down. This is done in the client, not the server. Your job is to make every bitrate step available at the CDN as a separate HTTP resource, so the client can switch mid-playback without renegotiation.
The pre-encoding cost is real: one title × 150 encodings takes tens of thousands of CPU-hours. But it's a one-off — do it once when the title lands, and then serve it forever. Netflix pushed harder than anyone here by encoding per-title, per-scene, per-shot — the ladder isn't fixed, it's optimised for the specific content.
The OCA: a CDN you build because the public one is too shallow
A generic CDN caches at ~200 PoPs. That's still 30-50 ms from a user in a smaller city, and every cache miss traverses the ISP's uplink. Netflix's Open Connect Appliance goes one hop deeper: a rack of 100 TB of storage sitting inside the ISP's network. The ISP saves transit bandwidth (Netflix is often ~15% of their total), Netflix gets a cache hit rate approaching 100% for popular content, and users see 5 ms first-byte times.
The catch: OCAs must be filled before the traffic arrives. Every night, a control plane predicts what will be watched tomorrow (new releases, catalogue titles heavy in that region) and pushes new bytes down during off-peak hours — the ISP's uplink is nearly free at 3 am. The prediction is fuzzy but the tail matters little: cold-content misses fall through to a regional cache, then to origin, at the cost of a few tens of milliseconds extra.
Personalisation without ML on the hot path
The home page is heavily personalised — ranked rows of titles the model thinks you want. If you did that scoring on request, every page load runs an ML inference, and the p99 explodes. The pattern Netflix leans on: compute rows offline (nightly, or every few hours), keyed by profile_id, and write them to Redis. The home service on the hot path is a Redis GET — a few milliseconds.
The staleness is acceptable because the row set is a menu, not a real-time signal. Fresher signals — "continue watching" — are computed on demand from a small window of recent history in a separate service. Layer them together at render time and you get a personalised page in <100 ms without a single model call per request.
Follow-ups you should expect
How is this different from YouTube?
How do you handle simultaneous-stream limits?
What breaks on a Sunday night season-finale release?
/home and /titles/{id}/playback at once. Cache aggressively (per-title playback URLs cache for minutes), rate-limit at the edge, and pre-scale the home service.How would you add live streaming (a boxing match)?
Rights vary by country — where does that check live?
rights_by_country for the title against the client's inferred region and returns a manifest only for allowed content. The CDN doesn't know or care about rights — the URL signing is the enforcement mechanism.Where candidates lose the room
- Treating this like YouTube and inventing an upload pipeline — the catalogue is closed and the interviewer knows it.
- Serving media through your API tier or your general CDN — every design decision hinges on getting bytes into ISP-embedded caches.
- Running ML on the request path for home page personalisation — you've turned a Redis GET into an inference call.
- Ignoring the encoding step because it's "offline" — the ladder structure is what makes ABR work, and interviewers want to see you understand it.
- Storing watch history in Postgres — a linear-per-user log is what Cassandra is for; SQL joins are pointless when the access pattern is time-slice-by-user.