Phase 5 · Chapter 3
System Design: YouTube
When the bottleneck is not your database — it's physics, bandwidth, and CPU
What this chapter does to you: YouTube is a different category of problem from URL Shortener and Twitter. The bottleneck is not reads per second or fan-out writes. The bottleneck is raw bytes — terabytes of video moving across the planet every second, and the CPU cost of transforming those bytes before they can move. After this chapter, you will understand video transcoding pipelines, adaptive bitrate streaming, CDN architecture, and why a single viral video can serve a billion views without a single request hitting YouTube's origin servers.
Step 1: Clarify Requirements
Functional requirements:
- Users can upload videos
- Videos are processed and available for streaming within minutes of upload
- Users can watch videos at multiple quality levels (360p, 720p, 1080p, 4K)
- Video quality adjusts automatically based on the viewer's connection speed
- Users can like, comment, subscribe
- Users get a recommended feed of videos
- Search for videos
Non-functional requirements:
- 500 hours of video uploaded every minute (real YouTube stat)
- 1 billion hours of video watched per day
- Videos must be globally accessible with low latency
- Upload must be reliable — a network hiccup should not restart the entire upload
- Viewing must be smooth — buffering is the enemy
What's out of scope (confirm in interview):
- Recommendation algorithm internals (ML system — separate discipline)
- Live streaming (different architecture)
- YouTube Shorts (similar but different processing pipeline)
The single most important insight from these requirements:
"500 hours uploaded per minute" means your system must process video faster than it arrives. If processing takes longer than upload, you build an unbounded backlog. This constraint alone defines the entire architecture.
Step 2: Estimate Scale
Upload volume:
- 500 hours of video per minute = 8.3 hours/second
- Average raw video: 1 hour of 1080p ≈ 2 GB uncompressed
- 8.3 hours/sec × 2 GB = ~16.6 GB of raw video per second arriving
- Per day: 16.6 GB/s × 86,400 = ~1.4 PB of raw video per day
Processed output (multiple resolutions per video):
- Each uploaded video gets transcoded into: 360p, 480p, 720p, 1080p, 2160p (4K)
- Compression (H.264/H.265) reduces size dramatically
- 1 hour of 1080p H.264 ≈ 8 GB (from 2 GB raw — but you're actually re-encoding)
- 5 resolutions per video ≈ 15-25 GB of processed output per hour of input
- Per day: ~20 PB of processed video stored
Viewing volume:
- 1 billion hours watched/day = ~11.6 million hours/second
- Average stream: 720p ≈ 1 Mbps
- 11.6M hours/sec × 1 Mbps = 11.6 Tbps of outbound bandwidth
- No single company's origin servers can serve 11.6 terabits per second.
- This single number tells you the entire CDN strategy before you've drawn a box.
What the estimates tell you:
- Video processing is CPU-bound, not I/O-bound. You need massive parallel CPU.
- Storage is in petabytes — object storage (S3), not Postgres.
- Serving is a CDN problem, not an origin server problem.
- The database stores metadata, not video. Video never touches your DB.
Step 3: What Video Actually Is — The Physical Reality
Before designing the system, you must understand what you're dealing with.
Raw Video
A video file is a sequence of frames — images displayed in rapid succession. 30fps video = 30 still images per second. 1920×1080 pixels each. Each pixel = 3 bytes (RGB) = ~6 MB per frame uncompressed. 30 frames/sec × 6 MB = 180 MB/second of raw video data. 1 hour = 180 MB/s × 3600 = 648 GB of raw video for 1 hour.
Obviously you cannot store or stream 648 GB per hour. Video codecs compress this.
Video Codecs — Compression
A codec (coder-decoder) compresses video by exploiting two facts:
Fact 1: Adjacent frames are similar. If you're filming someone talking, the background doesn't change between frames. Instead of storing the full frame 2, store only what changed from frame 1. This is called inter-frame compression (P-frames and B-frames in H.264).
Fact 2: Human eyes are less sensitive to color detail than brightness. You can reduce color resolution by 2× without most people noticing. This is called chroma subsampling (4:2:0 in most video).
Combined, H.264 compresses 648 GB/hour to ~8 GB/hour at 1080p — 80× compression. H.265 (HEVC) achieves similar quality at ~4 GB/hour — 160× compression. AV1 (Google's codec, used on YouTube) achieves ~2-3 GB/hour — 200× compression.
The codec tradeoff: Better compression = more CPU to encode and decode. H.264 is universal — every device supports hardware decoding. AV1 is 30-50% more efficient than H.264 but requires newer hardware for decode. YouTube uses AV1 for desktop/newer mobile, H.264 as fallback.
Container Formats
A codec compresses the video stream. A container packages video + audio + metadata.
Common containers: MP4 (H.264/H.265), WebM (VP9/AV1), MKV (anything).
YouTube accepts almost any input format and normalizes everything internally. Output to viewers is MP4 or WebM depending on browser support.
Adaptive Bitrate Streaming (ABR) — The Most Important Concept
You've experienced this: you start a YouTube video, it plays in 360p, then switches to 1080p as your connection proves itself. This is Adaptive Bitrate Streaming.
How it works:
Each video is encoded into multiple versions (called renditions):
360p — 400 kbps — for slow connections
480p — 800 kbps — for mobile
720p — 1.5 Mbps — standard HD
1080p — 4 Mbps — full HD
4K — 15 Mbps — for fast connections
Each rendition is then split into small segments (2-4 seconds each).
A manifest file (.m3u8 for HLS, .mpd for DASH) lists all available
renditions and their segment URLs:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=400000,RESOLUTION=640x360
https://cdn.youtube.com/video/abc/360p/segment_%d.ts
#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720
https://cdn.youtube.com/video/abc/720p/segment_%d.ts
#EXT-X-STREAM-INF:BANDWIDTH=4000000,RESOLUTION=1920x1080
https://cdn.youtube.com/video/abc/1080p/segment_%d.ts
The video player:
- Downloads the manifest file
- Measures current download speed
- Picks the highest rendition that fits within available bandwidth
- Downloads segments one by one
- After each segment, re-measures bandwidth and switches rendition if needed
Why segments matter:
If you encoded the entire video as one file, switching quality mid-video would require seeking in the file and re-downloading from the new position. With 2-4 second segments, the player just fetches the next segment from a different quality folder. Seamless switch at every segment boundary.
This is why YouTube can switch from 360p to 1080p mid-video without buffering — it was already playing segments. The next segment just comes from a different URL.
Step 4: The Upload Pipeline
This is where most candidates get it wrong. They say "user uploads video to server." But a 2 GB video upload to a single server has fatal problems:
- Network failure at 1.9 GB = start over from zero
- One server handling all uploads = single point of failure
- Processing on the same server as upload = resource contention
The correct design: separate upload from processing, and make upload resumable.
Resumable Upload — Chunk-Based
This is the resumable upload protocol (Google's TUS protocol or similar). The server tracks bytes received per session. Clients can resume from any interruption without re-uploading completed chunks.
Implementation:
// Initiate upload
app.post('/api/upload/init', async (req, res) => {
const { filename, fileSize, contentType } = req.body
const uploadId = generateId()
await redis.hset(`upload:${uploadId}`, {
filename,
fileSize,
bytesReceived: 0,
status: 'uploading'
})
res.json({ uploadId, uploadUrl: `/api/upload/${uploadId}` })
})
// Receive chunk
app.patch('/api/upload/:uploadId', async (req, res) => {
const { uploadId } = req.params
const { rangeStart, rangeEnd } = parseRangeHeader(req.headers['content-range'])
// Write chunk to object storage (S3/R2) at the correct byte offset
await objectStorage.uploadPart(uploadId, rangeStart, req.body)
await redis.hset(`upload:${uploadId}`, 'bytesReceived', rangeEnd + 1)
const { fileSize } = await redis.hgetall(`upload:${uploadId}`)
if (rangeEnd + 1 >= parseInt(fileSize)) {
// Upload complete — trigger processing pipeline
await redis.hset(`upload:${uploadId}`, 'status', 'processing')
await processingQueue.add('transcodeVideo', { uploadId })
res.status(200).json({ status: 'processing' })
} else {
res.status(308).json({ bytesReceived: rangeEnd + 1 }) // 308 = Resume Incomplete
}
})
Where the Raw File Goes
Raw video goes directly to object storage (S3, GCS, or Cloudflare R2). Never to your application server's disk. Why?
- Object storage is infinitely scalable — you don't manage capacity
- Multi-part upload is built in — each chunk uploaded independently
- Object storage is durable — typically 11 nines (99.999999999%) durability
- Your app servers are stateless — they can crash and restart without losing the upload
- Object storage can generate signed URLs — clients can upload directly, bypassing your app server entirely
Direct-to-S3 upload (the production pattern):
// Client requests a pre-signed upload URL — expires in 15 minutes
app.post('/api/upload/presign', async (req, res) => {
const key = `raw/${uploadId}/${filename}`
const presignedUrl = await s3.getSignedUrl('putObject', {
Bucket: 'raw-video-bucket',
Key: key,
Expires: 900, // 15 minutes
ContentType: req.body.contentType
})
res.json({ uploadUrl: presignedUrl, key })
})
The client uploads 2 GB directly to S3. Your app server never handles a single byte of the video. Your server's bandwidth is not the bottleneck.
Step 5: The Transcoding Pipeline — The Hard Part
A raw video arrives in S3. You must produce 5 renditions (360p through 4K). Each rendition requires CPU-intensive re-encoding.
Transcoding cost:
- 1 hour of 1080p → 720p transcode ≈ 30 minutes on one CPU core
- 1 hour → all 5 renditions = 150 minutes on one CPU core
- 500 hours uploaded per minute = 750,000 CPU-minutes of transcoding per minute
- You need 750,000 CPU cores running continuously just to keep up
This is why YouTube uses massively parallel transcoding workers.
The Transcoding Queue
// Triggered when upload completes
await processingQueue.add('transcodeVideo', {
videoId,
rawKey: 'raw/abc123/video.mp4',
resolutions: ['360p', '480p', '720p', '1080p', '2160p']
})
The fan-out pattern from Chapter 4 (Phase 4 — Queues) applies here:
Don't transcode all resolutions sequentially on one worker. Fan out: one job per resolution, all run in parallel.
worker = new Worker('transcodeVideo', async (job) => {
const { videoId, rawKey, resolutions } = job.data
// Fan out: create one transcoding job per resolution
await Promise.all(resolutions.map(resolution =>
transcodingQueue.add('transcodeResolution', {
videoId,
rawKey,
resolution,
outputKey: `processed/${videoId}/${resolution}/`
})
))
})
// Separate workers per resolution — run on different machines
worker = new Worker('transcodeResolution', async (job) => {
const { rawKey, resolution, outputKey } = job.data
// Download raw video from S3 (or stream it — don't buffer 2GB in RAM)
// Run FFmpeg to transcode
// Split output into 2-second segments
// Upload segments to CDN origin
await ffmpeg.transcode({
input: `s3://${rawKey}`,
output: `s3://${outputKey}`,
resolution,
segmentDuration: 2,
codec: 'libx264',
preset: 'veryfast' // tradeoff: faster encoding, slightly larger output
})
// Notify completion
await completionQueue.add('resolutionComplete', { videoId, resolution })
})
The Completion Tracking Problem
5 resolutions, 5 parallel workers. How do you know when all 5 are done so you can mark the video as available?
Pattern: distributed counter with threshold notification
worker = new Worker('resolutionComplete', async (job) => {
const { videoId, resolution } = job.data
// Mark this resolution as done
await redis.sadd(`video:${videoId}:done_resolutions`, resolution)
// Check if all resolutions complete
const doneCount = await redis.scard(`video:${videoId}:done_resolutions`)
const totalResolutions = 5
if (doneCount === totalResolutions) {
// All done — generate manifest file, mark video as available
await generateManifest(videoId)
await db.query(
'UPDATE videos SET status = $1 WHERE id = $2',
['available', videoId]
)
await notifyUploader(videoId)
redis.del(`video:${videoId}:done_resolutions`)
}
})
This is the BullMQ Flow pattern from Chapter 4 (Phase 4), implemented manually.
SADD + SCARD is the atomic way to track progress across distributed workers.
FFmpeg — The Tool Behind Every Video Platform
FFmpeg is an open-source tool that handles encoding, decoding, transcoding, filtering, and streaming of audio and video. Every major video platform uses it internally (or a wrapper around it).
# Transcode a video to 720p, segment into 2-second HLS chunks
ffmpeg -i input.mp4 \
-vf scale=-2:720 \ # scale to 720p, maintain aspect ratio
-c:v libx264 \ # H.264 codec
-crf 23 \ # quality (0=best, 51=worst, 23=default)
-preset veryfast \ # encoding speed vs compression tradeoff
-c:a aac \ # audio codec
-hls_time 2 \ # 2-second segments
-hls_segment_filename "segment_%03d.ts" \
output.m3u8 # manifest file
This single command is the core of your transcoding worker. The complexity is in managing it at scale: 750,000 CPU cores, error handling, retry on failure, cost optimization (spot instances), output validation.
Step 6: CDN Architecture — Serving at 11.6 Tbps
Your origin servers cannot serve 11.6 terabits per second. No company's servers can. The answer is a CDN.
What a CDN Actually Does
A CDN (Content Delivery Network) is a network of edge servers geographically distributed around the world. Mumbai, São Paulo, Lagos, Tokyo, Frankfurt — thousands of locations.
When a user in Mumbai watches a YouTube video:
Without CDN:
User (Mumbai) → YouTube servers (USA) → 200ms RTT each segment
Each 2-second segment request = 200ms latency before playback starts
Buffering every few segments
With CDN:
User (Mumbai) → CDN edge (Mumbai) → ~5ms RTT each segment
First request for this video: edge fetches from origin, caches locally
All subsequent requests: served from Mumbai edge, no origin involved
The CDN edge caches popular video segments. Mumbai users watching a viral Indian music video all hit the Mumbai edge — that video is fetched from origin exactly once, then served to millions of local users.
The Cache Hierarchy
Cache hit rates: Top 1% of videos = 99% of views. CDN edge caches that 1%. For the most popular videos (billion views), the CDN cache hit rate is ~99.9%. Origin serves the first copy and the rare 0.1% miss.
For the long tail (rarely watched videos): A video with 50 views will not be in any CDN cache. Every viewer request goes to origin. This is fine — 50 views is tiny traffic. The CDN optimizes the common case; origin handles the rare case.
Signed URLs for CDN Security
You can't let anyone access any video at any CDN URL — paid content, region-locked videos, private videos need access control.
// When user requests to watch video:
app.get('/api/watch/:videoId', authenticateUser, async (req, res) => {
const video = await getVideo(req.params.videoId)
// Check access rights
if (!canWatch(req.user, video)) return res.status(403).json({ error: 'Forbidden' })
// Generate signed CDN URL — expires in 1 hour
const signedManifestUrl = cdn.signUrl({
url: `https://cdn.youtube.com/videos/${video.id}/manifest.m3u8`,
expires: Date.now() + 3600000,
key: CDN_SIGNING_KEY
})
res.json({ manifestUrl: signedManifestUrl })
})
The CDN verifies the signature on every request. Expired or tampered URLs return 403. You sign the manifest URL once — the player uses it to fetch all segments, but each segment URL is derived from the signed manifest, inheriting its validity.
Step 7: The Data Model
Videos Table
CREATE TABLE videos (
id BIGINT PRIMARY KEY, -- Snowflake ID (from Chapter 2)
uploader_id BIGINT NOT NULL,
title VARCHAR(100) NOT NULL,
description TEXT,
status VARCHAR(20) DEFAULT 'processing',
-- 'processing' | 'available' | 'deleted' | 'rejected'
duration_sec INT,
thumbnail_url TEXT, -- CDN URL to thumbnail image
raw_key TEXT, -- S3 key for raw upload
view_count BIGINT DEFAULT 0, -- denormalized counter
like_count BIGINT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_videos_uploader ON videos(uploader_id, created_at DESC);
CREATE INDEX idx_videos_status ON videos(status) WHERE status = 'available';
Video Renditions Table
CREATE TABLE video_renditions (
video_id BIGINT NOT NULL,
resolution VARCHAR(10) NOT NULL, -- '360p', '720p', '1080p', etc.
manifest_url TEXT NOT NULL, -- CDN URL to .m3u8 file
file_size_mb INT,
bitrate_kbps INT,
codec VARCHAR(20), -- 'h264', 'av1'
PRIMARY KEY (video_id, resolution)
);
Comments — A Deep Tree Problem
Comments on YouTube have replies. Replies can have replies (one level in YouTube's case). This is a tree structure stored in a relational DB.
CREATE TABLE comments (
id BIGSERIAL PRIMARY KEY,
video_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
parent_id BIGINT, -- null = top-level comment
content TEXT NOT NULL,
like_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_comments_video ON comments(video_id, created_at DESC);
CREATE INDEX idx_comments_parent ON comments(parent_id);
Top-level comments: WHERE video_id = $1 AND parent_id IS NULL ORDER BY like_count DESC
Replies to a comment: WHERE parent_id = $1 ORDER BY created_at ASC
Step 8: View Counts — The High-Frequency Counter at Extreme Scale
A viral video gets 1 million concurrent viewers.
If each view increments view_count in Postgres:
- 1M
UPDATE videos SET view_count = view_count + 1per second - On one row
- All 1M updates compete for the same row lock
- Complete deadlock. Nothing works.
This is the same problem as the like counter in Chapter 2 (Twitter), but 1,000× worse because concurrent viewers are all active simultaneously.
Solution: Distributed Counter in Redis + Periodic Flush
Exact same write-behind pattern from Twitter, but with one critical addition — sharding the counter to eliminate the single-key hotspot:
// Instead of one key per video, use N shards
const SHARD_COUNT = 100
async function recordView(videoId) {
const shard = Math.floor(Math.random() * SHARD_COUNT)
const key = `views:${videoId}:shard:${shard}`
await redis.incr(key)
}
// Periodic aggregation job (every 30 seconds):
async function aggregateViews(videoId) {
let total = 0
const pipeline = redis.pipeline()
for (let i = 0; i < SHARD_COUNT; i++) {
pipeline.getdel(`views:${videoId}:shard:${i}`) // get and delete atomically
}
const results = await pipeline.exec()
total = results.reduce((sum, [err, val]) => sum + (parseInt(val) || 0), 0)
if (total > 0) {
await db.query(
'UPDATE videos SET view_count = view_count + $1 WHERE id = $2',
[total, videoId]
)
}
}
Why sharding?
One Redis key for view counts = one key gets 1M INCR per second.
Redis is single-threaded. Even at 100k ops/second capacity,
1M ops/second on one key = queue depth grows unboundedly.
100 shards = 10,000 INCR per shard per second. Well within Redis capacity.
Aggregation combines them every 30 seconds. DB sees 1 update per 30 seconds per video.
The consistency tradeoff: View count might be 30 seconds stale. For YouTube, this is completely acceptable. View count is not used for financial decisions — it's a social signal.
Step 9: Video Recommendations — What You Need to Know for Interviews
You don't need to design the ML model. You need to design the serving layer.
The ML model produces recommendations offline (batch job, runs every few hours). Output: for each user, a ranked list of video IDs.
user:123 → [videoId:456, videoId:789, videoId:012, ...] (ranked by predicted engagement)
This is stored in a fast key-value store (Redis or a specialized feature store):
// ML pipeline writes recommendations
await redis.zadd(`recommendations:${userId}`,
{ score: 0.95, member: 'video:456' }, // higher score = higher rank
{ score: 0.87, member: 'video:789' },
// ...
)
// Feed API reads them
async function getRecommendedFeed(userId) {
const videoIds = await redis.zrevrange(`recommendations:${userId}`, 0, 19)
return Promise.all(videoIds.map(id => getVideoMetadata(id)))
}
Real-time signals (user just watched X, so show Y) are handled by a streaming pipeline (Kafka → feature computation → real-time recommendation update). This is out of scope but worth naming in an interview.
Step 10: The Complete Architecture
Step 11: Decisions Table
| Decision | Why |
|---|---|
| Direct-to-S3 upload | App server never touches video bytes. Resumable. Scalable. |
| Chunk-based resumable upload | 2 GB upload + one network drop = complete loss without it |
| Separate processing queue | Upload completes instantly, processing happens async |
| Fan-out per resolution | 5 resolutions in parallel = 5× faster than sequential |
| HLS with 2-second segments | Enables adaptive bitrate switching at every segment boundary |
| CDN for video delivery | 11.6 Tbps cannot come from origin. CDN is the only answer. |
| Signed CDN URLs | Access control without hitting origin on every request |
| Sharded view counter | 1M concurrent viewers can't all hit one Redis key |
| Write-behind for counters | DB cannot absorb 1M view-count updates/second |
| Snowflake IDs for videos | Time-sortable, globally unique, no DB coordination needed |
| Postgres for metadata only | Video bytes never touch DB. Petabytes in object storage. |
Practice Problems
Problem 1 (The Codec Tradeoff): YouTube is moving from H.264 to AV1 encoding. AV1 produces 40% smaller files at the same quality.
For your infrastructure:
- What changes in storage cost? (Calculate from the 20 PB/day estimate)
- What changes in CDN bandwidth cost?
- What is the tradeoff cost on the encoding side?
- Which users would you roll AV1 out to first, and why?
Problem 2 (Processing Pipeline Failure): Your transcoding workers are processing a video. The 720p worker finishes successfully and uploads its segments to the CDN. The 1080p worker crashes mid-encode after processing 40 minutes of a 60-minute video.
What state is the system in? How does BullMQ handle this (refer to stall detection from Chapter 4, Phase 4)? When the 1080p worker retries, does it re-encode from the beginning or from minute 40? What would you need to implement to make it resume from minute 40?
Problem 3 (CDN Cache Invalidation): A creator uploads a video. It gets processed and served from CDN. They realize the thumbnail is wrong and update it.
The thumbnail is cached at 2,000 CDN edge nodes around the world. How do you invalidate it? What is the cost of that operation? How does this connect to the cache invalidation problem from Chapter 1, Phase 4? Is there a design that avoids the invalidation problem entirely?
Problem 4 (Scale Calculation): YouTube reports 1 billion hours watched per day at an average of 720p (1.5 Mbps).
Calculate:
- Total outbound bandwidth in Tbps
- If CDN cache hit rate is 99.5%, how much bandwidth hits origin servers?
- If each CDN PoP serves a 100 Gbps uplink, how many PoPs do you need globally?
- If a PoP costs $500,000/month, what is the monthly CDN infrastructure cost for bandwidth alone?
Problem 5 (Connect Everything): A user uploads a 2-hour 4K video on AttemptIQ (imagine you add video lectures).
Trace the entire journey:
- Upload: which protocol, where do bytes go, what makes it resumable?
- Processing: what queue pattern, how many workers, how do you know when done?
- Storage: where do raw and processed files live, why not Postgres?
- Delivery: why can't your Hetzner server serve this, what layer handles it?
- Viewing: what happens in the browser when the video player requests quality switch?
Use exact vocabulary from every chapter in this curriculum to answer. This problem spans Phase 1 (TCP/HTTP), Phase 2 (DB design), Phase 4 (Queues, Caching), and Phase 5.
Where We Are in Phase 5
Phase 5 — System Design HLD
✅ Chapter 1: URL Shortener (read-heavy, caching, ID generation)
✅ Chapter 2: Twitter Feed (fan-out, write amplification, social graph)
✅ Chapter 3: YouTube (media pipeline, CDN, adaptive streaming)
⬜ Chapter 4: WhatsApp (messaging, real-time delivery, end-to-end encryption)
⬜ Chapter 5: Uber (geospatial, real-time matching, surge pricing)
⬜ Chapter 6: Google Docs (real-time collaboration, OT vs CRDT)
⬜ Chapter 7: Notifications (fan-out at scale, multi-channel delivery)
Chapter 4 (WhatsApp) is a different beast again — the bottleneck is neither storage nor bandwidth, but real-time delivery to online/offline users, message ordering guarantees, and the single-copy delivery problem (a message must never be lost or shown twice, even when the sender retries).
End of Phase 5, Chapter 3. Estimated reading time: 80-100 minutes. Practice problems: 60-90 minutes. Do not proceed to Chapter 4 until you can answer Problem 2 completely — the transcoding worker crash and partial resume question. That problem requires you to connect FFmpeg chunking, BullMQ stall detection, and idempotency into one coherent answer. If you can reason through it cleanly, Phase 4 and Phase 5 have fused into a single mental model. That's the goal.