Case Study: Designing a Video Streaming Service
A full worked example for a Netflix/YouTube-style service — upload and transcoding pipeline, CDN delivery, and metadata database choices.
This case study applies the same process as the URL shortener case study — requirements, scale estimation, data model, architecture — to a system with a fundamentally different shape: instead of one simple read path, a video platform has two very different pipelines (a heavy, asynchronous upload/processing path, and a massive, latency-sensitive playback path) living inside one product, plus real binary-data storage and delivery problems a CRUD-style service never has to face.
1. Clarify requirements
Functional:
- Creators upload a video file.
- The platform processes each upload into a form suitable for streaming at varying quality levels.
- Viewers browse/search for videos and stream them with quality that adapts to their connection.
- (Explicitly out of scope for this design: recommendations, comments, and monetization — each is a substantial system in its own right.)
Non-functional:
- The playback path is read-heavy at a genuinely massive scale — millions of concurrent viewers — while the upload path is comparatively low-volume but resource-intensive in a completely different way (large binary files, CPU-heavy processing). These are two different problems, not one problem at two scales.
- Global audience: latency to start playback ("time to first frame") matters everywhere, and client bandwidth varies enormously, from fiber to a congested mobile connection.
- Storage at the scale of petabytes, growing continuously.
- High availability for playback specifically — a paying viewer expects video to simply work; a few extra minutes before a fresh upload becomes viewable is a completely acceptable trade-off by comparison.
2. Estimate scale
Assume: 10M daily active viewers, averaging 40 minutes watched/day
500K new uploads/month, averaging 10 minutes each
Peak concurrent streams (~20% of DAU during the peak viewing hour):
10,000,000 * 0.20 = 2,000,000 concurrent streams at peak
Peak egress bandwidth (~5 Mbps average per stream, adaptive bitrate):
2,000,000 * 5 Mbps = 10,000,000 Mbps = 10 Tbps at peak
-> at this scale, serving this directly from origin storage is not viable at all
Upload rate:
500,000 / (30 * 24 * 3600) ≈ 0.19 uploads/sec — trivial next to playback traffic
Storage:
500K videos/month * 10 min avg * ~50MB/min (source quality) ≈ 250 TB/month of raw source video
Transcoding into ~6 renditions roughly doubles-to-triples total stored bytes
-> ~600-750 TB/month, an easy multi-petabyte total within a few years
Conclusion: this is fundamentally a storage-and-bandwidth problem, not primarily a query or compute problem. The architecture has to be built around cheap bulk object storage, a transcoding pipeline that runs entirely off the critical path, and a CDN doing the overwhelming majority of actual byte delivery — the database and application-server layers matter, but they are not where this system lives or dies.
3. API design
POST /api/videos -- creator initiates an upload, receives a pre-signed upload URL
PUT {pre-signed upload URL} -- creator uploads the raw file directly to object storage
GET /api/videos/{id} -- video metadata: title, status, available renditions
GET /api/videos/{id}/manifest.m3u8 -- adaptive-bitrate manifest, the entry point for playback
4. The upload path and the transcoding pipeline
The raw file itself never passes through an application server — POST /api/videos only creates a metadata record and returns a pre-signed URL, and the creator's client uploads the (potentially multi-gigabyte) file directly to object storage. Routing large binary uploads through application servers just to relay them onward wastes their memory and bandwidth for no benefit.
Once the upload completes, object storage emits a completion event, which publishes a message onto a queue (exactly the decoupling pattern from this track's message-queues page) to kick off transcoding asynchronously. The upload response already returned; the creator sees a "processing" status while the pipeline runs in the background:
- Validate the upload — container format, codec, basic corruption checks.
- Segment the source file so a fleet of workers can transcode chunks in parallel rather than one worker processing the whole file serially — the same "why parallelize" reasoning behind a worker pool, applied to compute-heavy media jobs instead of concurrent requests.
- Transcode into multiple renditions — several resolutions (2160p/1080p/720p/480p/360p) at several bitrates, in a codec like H.264 for broad compatibility and/or AV1/HEVC for better compression at scale. The exact mix of renditions is itself a bandwidth-cost-versus-quality trade-off: more renditions mean smoother adaptive switching for viewers, at the cost of more storage and more transcoding compute per upload.
- Package into an adaptive bitrate streaming format — HLS (an
.m3u8manifest referencing small.ts/.fmp4segments) or MPEG-DASH. The manifest lists every available rendition; the player itself decides which one to request next, segment by segment, based on measured bandwidth, and can switch mid-playback without interrupting it. - Publish — generate a thumbnail and flip the video's status to "ready" as soon as the essential renditions exist, rather than blocking on every last rendition finishing.
Upload --> Object Storage (raw file) --event--> Queue --> Transcoding worker fleet
|
renditions written back to Object Storage
|
status updated to "ready" in the Metadata DB
5. CDN strategy for delivery
The load-balancing-and-caching page in this track covers CDNs briefly as one caching layer among several; a video platform is close to the canonical reason CDNs exist at all. At 10 Tbps of peak concurrent egress, origin object storage serving every byte directly to every viewer worldwide simply isn't an option — bandwidth and cost at that scale demand that the overwhelming majority of bytes never travel farther than a viewer's nearest edge location.
Adaptive bitrate streaming makes this especially effective: each segment is small (a few seconds of video) and independently cacheable, and a popular video's segments get requested by enormous numbers of different viewers — giving a CDN a very high cache hit rate for anything that's actually being watched, with almost none of that traffic ever reaching origin storage.
- Third-party CDN (CloudFront, Akamai, Fastly) is the standard choice, and the right default for the overwhelming majority of systems at this scale.
- A purpose-built CDN (Netflix's Open Connect is the best-known example) — physical appliances placed directly inside ISP networks — only becomes worth the enormous operational investment at a scale where the cost and control benefits outweigh building and running your own edge network; most systems never reach that point and shouldn't try to.
- An origin shield — an intermediate caching tier between edge points-of-presence and origin storage — absorbs the "cache miss stampede" that happens when a video suddenly goes viral and every edge location simultaneously misses on it, so origin storage sees one request instead of thousands.
6. Metadata database choice
"Metadata" here means everything about a video that isn't the video bytes themselves: title, description, owner, processing status, the list of available renditions and their storage locations, duration, view counts. This lives in a completely separate store from the video files, which stay in object storage exclusively.
The dominant access pattern is a simple key lookup by video ID — fetch metadata to render a watch page — at very high read volume, with a comparatively low write rate (status transitions during processing, occasional edits). A distributed wide-column/NoSQL store (Cassandra- or DynamoDB-style), partitioned by video ID, is a strong fit: simple key-based access scales horizontally without effort, and a few seconds of staleness on "has this finished processing yet" is completely harmless, so this store can comfortably favor availability (AP-leaning, per the CAP page) over strict consistency.
Browsing and searching by title or category is a fundamentally different access pattern that a key-value store isn't built for — a separate search index (Elasticsearch or similar), fed asynchronously from the metadata store, handles that instead of forcing one database to be good at two unrelated things.
View counts deserve special handling. A naive UPDATE videos SET views = views + 1 on every single playback creates an extreme write hotspot on exactly the rows — popular videos — that get hit hardest. The practical fix is to decouple the hot path from the source of truth entirely: increment a fast, in-memory counter (Redis) on every view, and periodically flush aggregated counts back to the metadata store in batches, trading perfectly-real-time accuracy (nobody needs a view counter accurate to the second) for a write pattern that scales.
7. CAP trade-off for this system
The playback path should clearly favor availability: a viewer seeing a view count or a rendition list that's a few seconds stale is unnoticeable, while the system failing to serve video at all is immediately and universally noticed. The upload/ownership path leans the same way in practice, even for operations that sound like they need strict consistency — a takedown or deletion propagating within seconds, rather than instantaneously across every cache and CDN edge, is normally acceptable.
The reasoning here differs from the URL shortener case study in an important way: the shortener leans AP because staleness is harmless and the system is trivially low-volume. This system leans AP largely because of sheer delivery scale — correctness for the genuinely sensitive hot spots (like view counts) is handled by specific design choices for that data, rather than by picking one blanket consistency model and applying it uniformly everywhere.
8. Summary architecture
Creator --> Object Storage (raw upload) --event--> Queue --> Transcoding worker fleet
|
renditions --> Object Storage
|
Metadata DB (status, rendition URLs)
|
Viewer --> CDN (edge cache, HLS/DASH segments) --miss--> Origin Shield --miss--> Object Storage
|
+-- client's adaptive bitrate player selects a rendition per segment
Common mistakes
- Routing the raw video upload through application servers instead of directly to object storage via a pre-signed URL — needlessly burns app-server memory and bandwidth on files that can be many gigabytes each.
- Blocking the upload response on transcoding finishing — a large file can take many minutes to process; this must be asynchronous, with status polled or pushed to the creator separately.
- Serving playback directly from origin storage "because it works fine in testing" — testing never reproduces millions of concurrent global viewers, and origin egress cost and capacity become catastrophic at real scale without a CDN in front of it.
- Treating view counts (and similar extremely hot, low-consistency-need counters) the same as ordinary metadata writes — an exact synchronous increment per view creates a severe write hotspot on exactly the videos getting the most traffic.
- Choosing one database to handle both key-based metadata lookups and text search/browsing — these are different access patterns, best served by two specialized stores rather than one store stretched to do both adequately.
Interview questions
Q: Why does the upload path send the raw video file directly to object storage instead of through the application's API servers? A pre-signed URL lets the client upload straight to object storage, so multi-gigabyte files never pass through — and consume the memory and bandwidth of — the application tier at all. The API server's only job in the upload path is issuing that URL and tracking metadata, not relaying file bytes.
Q: Why can't this system rely on origin storage alone to serve playback traffic, and what makes video segments especially cache-friendly? At millions of concurrent viewers, peak egress reaches many terabits per second — far beyond what origin storage could serve directly, and prohibitively expensive even if it could. Adaptive-bitrate streaming splits video into small, independently-cacheable segments that many different viewers of the same popular video request identically, giving a CDN a very high cache hit rate and letting the overwhelming majority of bytes reach viewers without ever touching origin storage.
Q: How would you handle view counts for a video that suddenly goes viral without creating a database write bottleneck? Decouple the increment from the source of truth: increment a fast in-memory counter (Redis) on every view, then periodically flush aggregated totals back to the metadata store in batches. This trades perfect real-time accuracy — which nothing actually needs for a view counter — for a write pattern that scales instead of hammering one hot row on every single playback.