Design Netflix

Design a subscription video-on-demand service. Members open the app on a TV, laptop or phone, browse a personalised home screen, and watch films and series from a catalogue of tens of thousands of titles. They expect playback to start in a couple of seconds, look as sharp as their connection allows, and pick up exactly where they stopped, even on a different device.

Unlike a user-upload platform, the catalogue is small and known in advance, but evening peaks push enormous amounts of video at the same moment. A single new season can be the most-watched thing in the world on launch night.

In 45 minutes, cover how video gets close to viewers, how the player adapts quality, how playback resume works across devices, and where recommendations plug in.

Sketch on the HLD board, write your answer in the Design doc, and size it with the Estimator. Start the mock interview from the timer for a 45-minute run.

Design Path · Media & Storage · problem 6 of 18

Ask before you design. Try answering each yourself, then compare.

  1. How big are the catalogue and audience?
    Typical answer

    About 20,000 titles (a few hundred thousand encoded files) and 200 M members, with 100 M streaming on a typical day.

  2. What does peak look like?
    Typical answer

    Evening prime time in each region carries roughly 3× the daily average; global peak concurrency is around 30 M streams.

  3. How precise must resume be?
    Typical answer

    Within about 10 seconds, visible on another device within a minute. Losing the last few seconds after a crash is acceptable.

  4. Do we own the encoding pipeline?
    Typical answer

    Yes, but studio deliveries are infrequent; treat encoding as an offline batch job and focus on delivery.

  5. Are recommendations part of the design?
    Typical answer

    Only the integration: how home rows are served fast and how viewing data feeds the models. Model internals are out of scope.

  6. Do we need offline downloads and DRM?
    Typical answer

    DRM licences yes, as a step before playback. Downloads are a follow-up.

The catalogue is known ahead of time. What can you do with that before the evening peak?
Which part of the stack carries 99% of the bytes, and where should it physically sit?
The player, not the server, decides which quality to fetch next. What inputs does it need?
Resume position is written constantly but read rarely. How often must it really hit the database?

Requirements

Functional

  • Browse a personalised home screen and title pages.
  • Start playback with a DRM licence and a manifest of available renditions.
  • Stream with adaptive bitrate that responds to network changes without stalling.
  • Resume from the last position on any device; show a 'continue watching' row.

Non-functional

  • Playback start under 2 s at p90; rebuffer ratio under 0.2%.
  • 99.99% availability for playback start; browsing may degrade to non-personalised rows.
  • Peak ~30 M concurrent streams and tens of Tbps of egress.
  • Resume position durable within ~1 minute and readable across devices.

Out of scope

  • Billing, sign-up and profile management.
  • Recommendation model training details.
  • Live events and user-generated content.

Back-of-the-envelope estimates

  • Streaming: 100 M daily viewers × 2 hours × ~5 Mbps average ≈ 450 PB per day, ~40 Tbps averaged and ~120 Tbps at peak.
  • Playback starts: ~3 titles per viewer per day = 300 M per day, ~3,500/s average and ~10,000/s at peak, each needing a licence and a manifest.
  • Resume writes: one checkpoint per minute of viewing is 120 per viewer per day; the client coalesces them so the server sees ~30 per viewer, about 35,000 writes/s on average and 100,000/s at peak, each ~200 bytes.
  • Catalogue storage: 20,000 titles × ~1.5 hours × ~120 encodes (resolutions, codecs, audio and subtitle tracks) ≈ a few PB in total, small enough to copy the popular part to every edge site.

Egress drives the design. Origin capacity and the control plane are modest; the question is how to place a few PB of mostly predictable content so that peak traffic never leaves the ISP.

API

GET  /v1/home?profile=p1                       -> {rows: [{title, items[]}], continueWatching[]}
POST /v1/play   {titleId, deviceCaps}          -> {manifest, edgeUrls[], licenseUrl, resumeAt}
POST /v1/license {challenge}                   -> {licence}
PUT  /v1/progress/{profile}/{titleId} {pos, ts} -> 204
GET  {edge}/c/{fileId}/range?bytes=a-b          -> video bytes

The play call returns a ranked list of edge URLs for this client, so the server steers traffic but the player can fail over on its own without another round trip.

Data model

titles(title_id PK, metadata, rights_by_region, files[]) in a replicated relational store; it changes rarely and is cached everywhere.

progress(profile_id PK, title_id CK, position_s, updated_at, device) in a wide-column store partitioned by profile, so one query returns a member's whole continue-watching list. Last-writer-wins on updated_at.

home_rows(profile_id PK, generated_at, rows) precomputed by the recommendation pipeline in a key-value store.

edge_inventory(site_id, file_id) and site health live in an in-memory edge directory used by steering. Encoded files live in object storage at origin.

High-level design

Browsing: the device calls the API gateway; the home service reads the member's precomputed rows from the recommendation store, fills in artwork and availability, and merges the continue-watching list from the progress service. If personalised rows are unavailable it falls back to regional popular rows.

Starting playback: the playback service checks entitlement and region rights, picks the files that match the device (codec, resolution, HDR), and asks the edge directory which healthy sites near this client hold those files. It returns a manifest, a ranked edge list, a licence URL and the resume position.

Streaming: the player fetches byte ranges from the first edge, typically a cache appliance inside the member's ISP, adapting bitrate segment by segment. If that edge is slow or missing a file it moves to the next one. Edges fill from origin object storage during off-peak hours based on the fill planner's popularity forecast; misses during the day fill from a nearby upstream site.

Progress: the player sends checkpoints; the progress service writes them to the progress store and also emits viewing events, which feed the recommendation pipeline that rebuilds home rows.

Open the HLD board and press Reference to see the architecture diagram.

Deep dives

Placing content at the edge

Because the catalogue is known, delivery can be planned rather than purely reactive. Deploy cache servers inside ISP networks and at internet exchanges. Each night, a fill planner forecasts what each region will watch tomorrow (new releases, trending titles, local language content) and pushes those files to the sites during off-peak hours. A typical site holds a few hundred TB, enough for the files behind well over 90% of its region's viewing. Big launches are pre-positioned days ahead. Steering then sends each client to sites that already hold the file, weighing proximity, load and health, so peak traffic rarely crosses transit links. Daytime misses fill from a regional tier, never directly from origin at scale.

Adaptive bitrate in the player

Each title is encoded into a ladder from roughly 235 kbps to 8+ Mbps, per codec, with per-title or per-scene tuning so simple animation uses fewer bits than grainy action. The player downloads a few seconds at a time and chooses the next rung from two signals: measured throughput and buffer occupancy. With a thin buffer it picks conservatively to avoid a stall; with a deep buffer it can try a higher rung. It climbs one rung at a time but drops immediately when throughput collapses. Starting at a moderate rung gives a fast first frame, and quality improves within seconds.

Resume position across devices

The player keeps position locally and sends a checkpoint every 30–60 seconds and on pause, stop or app background. The progress service writes it to a wide-column store keyed by profile with last-writer-wins on the client timestamp, plus a small sequence number per session to reject reordered writes. Reads are rare (home screen and play start), so they go straight to the store or a short-lived cache. If the store is slow, the service buffers writes in a queue and acknowledges, because losing 30 seconds of position is a minor annoyance, not data loss. Completed titles move the member to the next episode.

Serving recommendations fast

Ranking thousands of titles per member at request time would be slow and expensive. Instead, a pipeline consumes viewing events, recomputes candidate rows offline every few hours, and stores the result per profile. The home service only reads and lightly re-ranks it, for example to remove a just-finished title or boost a new episode. When the recommendation store is down, the home service serves cached or non-personalised regional rows, so browsing never blocks playback.

Failure modes

  • If an ISP edge site fails, steering removes it from edge lists within seconds and clients already playing fail over to the next URL on their list.
  • If the licence service is slow, playback cannot start, so it runs active-active in every region with cached keys; it is one of the few hard dependencies.
  • If the recommendation store is down, the home screen falls back to cached or popular rows instead of an error.
  • If the progress store is unavailable, checkpoints queue and resume falls back to the device's local position.
  • If an entire cloud region fails, control-plane traffic shifts to another region; streaming is mostly unaffected because bytes come from edges.

Core algorithm in code

LADDER_KBPS = [235, 560, 1050, 1750, 3000, 5800, 8000]


class AbrController:
    """Chooses the bitrate for the next segment from throughput and buffer level.

    Low buffer: be conservative. Healthy buffer: use most of measured throughput.
    Step up one rung at a time to avoid oscillation; drop as far as needed.
    """
    def __init__(self, ladder, segment_s: float = 4.0):
        self.ladder = ladder
        self.segment_s = segment_s
        self.estimate = None          # kbps, exponentially weighted
        self.current = ladder[0]

    def on_segment(self, size_kbit: float, download_s: float) -> None:
        sample = size_kbit / max(download_s, 1e-3)
        if self.estimate is None or sample < self.estimate:
            self.estimate = sample            # trust bad news immediately
        else:
            self.estimate = 0.7 * self.estimate + 0.3 * sample   # good news slowly

    def choose(self, buffer_s: float) -> int:
        if self.estimate is None:
            return self.current
        safety = 0.5 if buffer_s < 8 else 0.8 if buffer_s < 20 else 0.95
        budget = self.estimate * safety
        fits = [b for b in self.ladder if b <= budget] or [self.ladder[0]]
        target = fits[-1]
        i = self.ladder.index(self.current)
        if target > self.current:
            target = self.ladder[i + 1]   # climb one rung at a time
        self.current = target
        return target


# simulate a network that is good, collapses, then recovers
network_kbps = [9000] * 6 + [1200] * 5 + [6000] * 6
abr = AbrController(LADDER_KBPS)
buffer_s = 0.0
for step, bw in enumerate(network_kbps):
    rate = abr.choose(buffer_s)
    size = rate * abr.segment_s
    dl = size / bw
    stall = max(0.0, dl - buffer_s)
    buffer_s = max(0.0, buffer_s - dl) + abr.segment_s
    abr.on_segment(size, dl)
    print("seg %2d  net %5d kbps  chose %4d kbps  buffer %5.1f s%s"
          % (step, bw, rate, buffer_s, "  STALL %.1f s" % stall if stall > 0 and step > 0 else ""))

Trade-offs

  • Pre-positioning content in ISP sites makes peak traffic cheap and local but costs hardware, partnerships and a forecasting system that can guess wrong.
  • Precomputed home rows load in milliseconds but react to new viewing with hours of delay unless you add a real-time re-ranking layer.
  • Client-driven ABR adapts instantly to local conditions but means the server cannot directly control quality; steering and manifests are the only levers.
  • Last-writer-wins resume is simple and available but can occasionally jump back when two devices play the same title at once.

What a strong answer shows

  • Recognises egress as the dominant problem and places a known catalogue close to viewers ahead of demand.
  • Separates the control plane (cloud APIs) from the data plane (edge caches) and explains the failover between them.
  • Explains ABR with segment/range fetches, throughput and buffer signals.
  • Designs resume as a high-write, low-read, loss-tolerant path with sensible consistency.
  • Keeps recommendations off the critical path with precomputation and fallbacks.

Mistakes to avoid

  • Treating it like a user-upload site and spending the interview on uploads and transcoding.
  • Relying on a generic pull-through CDN only and never using the fact that demand is predictable.
  • Making the server choose quality per request instead of letting the player adapt.
  • Writing every resume heartbeat synchronously to a strongly consistent database.
  • Letting a recommendation outage block the home screen or playback.

Follow-ups to expect

  • How would offline downloads work? (Per-device licences with expiry and download limits, the same files as streaming.)
  • How would you launch a hugely anticipated season at midnight worldwide? (Pre-position days ahead, embargo via licence keys, pre-scale control plane.)
  • How do you measure streaming quality? (Client telemetry: startup time, rebuffers, bitrate; aggregate per ISP and site.)
  • How would you A/B test a new ABR algorithm? (Assign by member, compare rebuffer and bitrate distributions.)

Practice next

Next Post Previous Post
No Comment
Add Comment
comment url