Design Social & Streaming Systems
Solve complex real-world designs involving massive fan-out, feeds, and media delivery.
Why Social & Streaming Systems Are the Ultimate Design Challenge
Think about the last time you opened Instagram, hit play on Netflix, or sent a message in a group chat. The experience felt instant β almost magical. A feed of perfectly curated posts appeared in milliseconds. A 4K video buffered in less than a second. Your message was delivered before you even put your phone down. What you experienced was the invisible result of thousands of engineering decisions, each one carefully balanced against billions of others happening simultaneously. Understanding how that magic works is exactly what your interviewer wants to find out. Download the free flashcards for this lesson to keep key terms sharp as you read β you'll want them.
Social and streaming systems are not just popular interview topics because companies like Meta, Netflix, and Discord happen to be famous. They appear in interviews constantly because they are genuinely hard, and that hardness is multidimensional. These systems force you to reason simultaneously about scale, latency, consistency, fault tolerance, and cost β often in direct conflict with each other. A decision that makes your system faster might make it less consistent. A choice that saves storage costs might hurt your ability to serve global users quickly. Navigating those trade-offs, out loud, under pressure, while a senior engineer watches you β that is the interview.
This section is your entry point into the world of social and streaming system design. We'll establish why these systems are architecturally fascinating, what makes them so challenging, and how you should be thinking before you ever draw a single box on a whiteboard.
The Massive Scale Reality
Scale is not an abstraction. Let's put real numbers on the table so the challenge becomes viscerally concrete.
π€ Did you know? Meta's systems handle roughly 100 billion messages per day across its messaging platforms. YouTube processes over 500 hours of video uploaded every single minute. At peak, Netflix serves more than 15 petabytes of data per day to subscribers across 190 countries.
These numbers aren't trivia β they are the constraints your architecture must survive. Consider what "500 hours of video per minute" actually implies for a storage and processing system:
- Every minute, roughly 15 GB to 150 GB of raw video arrives (depending on resolution and codec)
- Each upload must be transcoded into multiple resolutions and formats (360p, 720p, 1080p, 4K, HLS, DASH)
- Each transcoded segment must be distributed to CDN edge nodes across the globe before the video even becomes watchable
- Metadata (title, description, tags, thumbnails, captions) must be indexed for search
- The whole pipeline must complete in minutes, not hours, or users abandon the upload
This is just the ingest side. On the delivery side, your system must serve millions of concurrent video streams while maintaining smooth playback and adapting to varying network conditions in real time. That single feature β video upload and playback β is itself a distributed systems problem of enormous complexity.
## Rough estimation of storage requirements for a video platform
## (This is the kind of back-of-envelope math you MUST do in interviews)
def estimate_storage_requirements():
"""
Back-of-envelope: Storage for a YouTube-scale platform
"""
hours_uploaded_per_minute = 500
minutes_per_day = 60 * 24 # 1,440 minutes
hours_per_day = hours_uploaded_per_minute * minutes_per_day # 720,000 hours/day
# Average 1 hour of video at 1080p β 4 GB (uncompressed much more, but post-encoding)
avg_gb_per_hour = 4 # GB at 1080p, compressed
replication_factor = 3 # store original + 2 replicas for durability
format_multiplier = 4 # transcode to 360p, 720p, 1080p, 4K
raw_gb_per_day = hours_per_day * avg_gb_per_hour
total_gb_per_day = raw_gb_per_day * replication_factor * format_multiplier
total_pb_per_day = total_gb_per_day / 1_000_000 # Convert GB to PB
print(f"Hours of video uploaded per day: {hours_per_day:,}")
print(f"Raw storage per day (before replication): {raw_gb_per_day:,} GB")
print(f"Total storage per day (with replication & formats): {total_gb_per_day:,} GB")
print(f"Roughly: {total_pb_per_day:.1f} petabytes per day")
estimate_storage_requirements()
## Output:
## Hours of video uploaded per day: 720,000
## Raw storage per day (before replication): 2,880,000 GB
## Total storage per day (with replication & formats): 34,560,000 GB
## Roughly: 34.6 petabytes per day
Running this kind of estimation in an interview immediately signals that you think in systems, not just code. It also anchors every subsequent design decision in reality. When your interviewer sees you derive "34+ petabytes per day," they know you understand why a single relational database simply cannot be the answer.
π― Key Principle: Scale changes the answer. A design that works perfectly for 10,000 users will collapse under 10 million. Every architectural decision in social and streaming systems must be stress-tested against realistic load projections before it's accepted.
Multiple Hard Problems, All at Once
What truly sets social and streaming systems apart is not that they are large β it's that they are large while simultaneously solving several distinct distributed systems problems. Most systems have one or two dominant challenges. Social and streaming platforms have at least five, all interlocked.
Real-Time Delivery
Real-time delivery means that when your friend posts a photo, you should see it almost instantly. When someone sends you a message, the notification should arrive in under a second. This requires WebSocket connections, long polling, or Server-Sent Events to maintain persistent channels between millions of clients and servers β at the same time.
Content Storage at Scale
Text, images, and video each have radically different storage and retrieval characteristics. A tweet is a few hundred bytes. A profile photo is a few hundred kilobytes. A 4K movie is tens of gigabytes. Your system must handle all three, often within the same user interaction, using fundamentally different storage technologies (object storage like S3 for media, relational databases for structured metadata, document stores or columnar databases for activity feeds).
Personalization and the Recommendation Engine
Every time you open TikTok, the algorithm has decided β in under 100 milliseconds β which video to show you first. This is not a simple lookup. It involves real-time signals (what you watched last, how long you watched it, what you liked), historical patterns, collaborative filtering across billions of users, and ranking models that run inference at query time. Personalization at scale is a machine learning engineering problem masquerading as a system design problem β and interviewers will probe whether you understand the infrastructure that makes it possible.
Global Distribution
A user in SΓ£o Paulo and a user in Singapore both expect the same low-latency experience. Achieving this requires Content Delivery Networks (CDNs), geographically distributed edge nodes, anycast routing, and replication strategies that propagate content to the right regions before users even request it. The physics of the speed of light means a round-trip from SΓ£o Paulo to a data center in Virginia adds ~180ms of irreducible latency. Good global distribution design makes that round-trip unnecessary.
Without CDN (naive design):
User (SΓ£o Paulo) βββββββββββββββββββββββββββββββΊ Origin Server (Virginia)
~180ms RTT
User waits for full round trip on every asset.
With CDN edge caching:
User (SΓ£o Paulo) βββββββΊ CDN Edge (SΓ£o Paulo) βββββββΊ Origin (Virginia)
~5ms [cache hit: serves locally]
[cache miss: fetches once, caches]
Subsequent users in SΓ£o Paulo get <10ms response.
Origin sees a fraction of the original request volume.
This diagram illustrates why CDN design is not optional at scale β it is the architecture.
π‘ Real-World Example: Netflix uses a system called Open Connect, a network of custom-built CDN appliances deployed inside ISP data centers around the world. By pre-positioning popular content inside the ISP's own network, Netflix reduces the distance data must travel to effectively zero for the most-watched content. The engineering insight: if you can predict what people will watch tonight, you can move the bits there before they press play.
Why Interviewers Love These Systems
A seasoned interviewer doesn't assign "Design Twitter" because they want you to rebuild Twitter. They assign it because it is the most efficient stress test of distributed systems knowledge available in a 45-minute window. Here's what they are actually measuring:
π Breadth of knowledge β Do you know when to use SQL vs. NoSQL? Do you know what a message queue is and why it helps? Can you name the trade-offs of eventual consistency vs. strong consistency?
π§ Depth on trade-offs β Anyone can draw boxes labeled "Database" and "Cache." Can you explain why you chose Cassandra over DynamoDB for the activity feed? Can you explain the fan-out problem and propose two different solutions with their respective costs?
π§ Practical judgment β Do you default to over-engineering, or do you make pragmatic calls? Do you know when "good enough" is correct?
π― Structured communication β Can you drive the conversation, handle ambiguity, and explain your reasoning to a non-specialist while a clock ticks?
β οΈ Common Mistake 1: Jumping straight into components. The moment an interviewer says "Design Instagram," many candidates immediately start drawing load balancers and databases. This signals panic, not mastery. The right move is to spend the first 5 minutes defining the problem before proposing any solution.
The CAP theorem β which states that a distributed system can guarantee at most two of Consistency, Availability, and Partition Tolerance β becomes vivid and testable in social systems. What happens to your news feed if a database partition occurs? Do users see stale data, or do they see an error? What does your system choose, and can you justify it?
β Wrong thinking: "I need a database and a cache. Done."
β Correct thinking: "My read-to-write ratio is probably 100:1 for a news feed. That tells me caching is critical. My writes are relatively low volume but must be durable. My reads can tolerate slight staleness β users won't notice if a post is 2 seconds delayed. That tells me I can accept eventual consistency on reads, which unlocks horizontal scaling through read replicas."
π― Key Principle: The CAP theorem isn't an academic concept in social systems β it's a daily engineering decision. Interviewers want to see you reason about which side of the trade-off your system lands on and why.
The Landscape of This Lesson
This lesson covers the foundational architecture that underlies all major social and streaming systems. Think of it as the DNA that different platforms share, even though their surfaces look very different.
The three canonical child topics you'll encounter in deeper lessons β News Feed design, Video Streaming design, and Chat system design β are not three unrelated problems. They are three expressions of the same underlying architectural patterns:
Shared Foundation
β
βββ News Feed Design
β βββ Fan-out on write vs. fan-out on read
β βββ Ranked feed generation
β βββ Cache invalidation strategies
β
βββ Video Streaming Design
β βββ Media ingestion and transcoding pipeline
β βββ Adaptive bitrate streaming (ABR)
β βββ CDN prefetching and edge caching
β
βββ Chat System Design
βββ Real-time message delivery (WebSockets)
βββ Message persistence and ordering
βββ Presence indicators and read receipts
As you study each topic, you'll notice that decisions made at the foundation β how you shard your database, how you structure your message queues, how you design your API gateway β ripple upward and constrain what's possible in each specialized domain. That's why this section comes first.
π Quick Reference Card: The Three Core Systems
| π― Primary Challenge | π¦ Key Storage | β‘ Latency Target | |
|---|---|---|---|
| ποΈ News Feed | Fan-out at write scale | Sorted sets (Redis), Column store | < 200ms feed load |
| π¬ Video Streaming | Transcoding + global delivery | Object storage (S3), CDN | < 2s video start |
| π¬ Chat | Real-time ordering + persistence | Message queue + append-only log | < 100ms delivery |
The Interview Mindset: Structure Before Solutions
One of the most counterproductive instincts engineers bring to system design interviews is the urge to solve immediately. We are trained, through years of coding problems, to jump to the answer. System design requires the opposite discipline: slow down, define, then design.
Here is a practical mental framework β call it the SCOPE method β for approaching any open-ended social or streaming prompt:
π§ Mnemonic β SCOPE:
- S β Scale: How many users? What's the read/write ratio? Daily active users?
- C β Core features: What are the 2-3 features we're actually designing? (Don't boil the ocean)
- O β Operational constraints: Latency requirements? Consistency requirements? Uptime SLA?
- P β Prioritization: What matters most? What can be V2?
- E β Estimation: Back-of-envelope math to ground your choices
Let's see this applied to a concrete prompt:
Interview Prompt: "Design a system like Twitter."
Weak response:
"Okay, so I'll have a load balancer, then an API server,
then a database... and maybe a cache..."
SCOPE response:
S β "Let's assume 300M daily active users. Tweet writes are
low volume (~500M tweets/day) but reads are enormous β
the home timeline is read billions of times per day."
C β "I'll focus on: (1) posting a tweet, (2) viewing the
home timeline, and (3) following a user. I'll leave
search and analytics for later."
O β "Timeline reads should be < 200ms. Tweets can tolerate
eventual consistency β if my follower sees my tweet
1-2 seconds late, that's fine. But tweet delivery
must be reliable β no lost tweets."
P β "The home timeline read path is the hardest problem.
That's where I'll spend the most design time."
E β "If 300M users each read their feed 10 times/day,
that's 3B read requests/day, or ~35,000 reads/second
at steady state. Peak is probably 5x that. My cache
layer needs to absorb the vast majority of these."
This structured opening takes three to five minutes and completely changes the quality of everything that follows. Your interviewer has learned more about your engineering judgment in those five minutes than most candidates reveal in forty-five.
## The kind of estimation function you should be able to write and explain
## in under 2 minutes during an interview
def twitter_scale_estimation():
"""
Back-of-envelope calculations for a Twitter-scale system.
Walking through this verbally demonstrates systems thinking.
"""
# Users
daily_active_users = 300_000_000 # 300M DAU
avg_follows_per_user = 200
# Write path: tweets
tweets_per_day = 500_000_000 # 500M tweets/day
tweets_per_second = tweets_per_day / 86_400 # ~5,800 TPS
# Read path: home timeline
timeline_reads_per_user_per_day = 10
total_reads_per_day = daily_active_users * timeline_reads_per_user_per_day
reads_per_second = total_reads_per_day / 86_400 # ~34,700 RPS steady state
peak_reads_per_second = reads_per_second * 5 # 5x peak factor
# Storage: tweets
avg_tweet_size_bytes = 300 # text + metadata
storage_per_day_gb = (tweets_per_day * avg_tweet_size_bytes) / 1e9
storage_per_year_tb = storage_per_day_gb * 365 / 1000
print("=== Twitter Scale Estimation ===")
print(f"Tweet write rate: {tweets_per_second:,.0f} tweets/sec")
print(f"Timeline read rate: {reads_per_second:,.0f} reads/sec (steady)")
print(f"Timeline read rate: {peak_reads_per_second:,.0f} reads/sec (peak)")
print(f"Read:Write ratio: {reads_per_second/tweets_per_second:.0f}:1")
print(f"Tweet storage/day: {storage_per_day_gb:.1f} GB")
print(f"Tweet storage/year: {storage_per_year_tb:.1f} TB")
print(f"\nConclusion: Read-heavy ({reads_per_second/tweets_per_second:.0f}x more reads than writes).")
print("Aggressive caching of timeline is the #1 priority.")
twitter_scale_estimation()
This output anchors the entire subsequent design. A read-to-write ratio of ~6:1 tells you immediately that you should optimize your architecture for reads β through caching, through pre-computed timelines, through read replicas β even if it adds complexity to the write path.
π‘ Pro Tip: Interviewers are not grading you on whether you designed the exact system that Twitter uses internally. They are grading you on whether your reasoning is sound, your trade-offs are conscious, and your design could plausibly survive the load you estimated. A clean, well-reasoned design for 80% of the requirements beats an incoherent attempt at 100%.
Setting the Stage
Everything you will learn in the remaining sections of this lesson β the service decomposition patterns, the media pipeline architecture, the caching and sharding strategies β exists to solve the problems we've surfaced here. Scale demands distributed storage. Real-time delivery demands persistent connections and message queues. Global users demand CDNs and edge computing. Personalization demands pre-computation and intelligent caching.
The systems are complex. But complexity, when understood structurally, becomes navigable. Your job in a system design interview is not to memorize Twitter's internal architecture. It is to demonstrate that you can reason your way to a credible, production-aware design from first principles.
The rest of this lesson gives you the building blocks to do exactly that.
Core Architecture Patterns for Social & Streaming Systems
Before you can design Twitter's timeline, Instagram's feed, or YouTube's recommendation engine, you need a shared vocabulary β a set of architectural blueprints that appear again and again across every large-scale social and streaming platform. This section builds that foundation. We'll work from the inside out: starting with the fundamental question of when to process data, moving through how services communicate asynchronously, then examining how to split a monolith into focused microservices, and finally confronting the hardest question in system design β which database do you actually use?
Fan-Out Patterns: The Heart of Feed Generation
Every social platform faces the same core challenge: when User A posts something, all of User A's followers need to eventually see it. How you solve this problem defines much of your system's character. There are two primary approaches, and choosing between them is one of the most important decisions you'll make in a social system design interview.
Write-time fan-out (also called push-on-write) means that the moment a user creates a post, your system immediately propagates that content to every follower's inbox or pre-computed feed cache. Think of it like a newspaper distributor who drops a copy on every subscriber's doorstep the moment the paper is printed.
Write-Time Fan-Out (Push Model)
User A posts βββΊ Post Service βββΊ Fan-Out Worker
β
ββββββββββββββββΌβββββββββββββββ
βΌ βΌ βΌ
Feed(B) Feed(C) Feed(D)
[Post A] [Post A] [Post A]
Read time: O(1) β just fetch pre-built feed
Write time: O(followers) β fan-out cost paid upfront
Read-time fan-out (also called pull-on-read) takes the opposite approach: the feed is assembled on the fly when a user requests it. Your system fetches the recent posts from every account the user follows, merges and ranks them, and returns a feed. Nothing is stored per-user until someone actually asks.
Read-Time Fan-Out (Pull Model)
User B requests feed
β
βΌ
Feed Service
β
Fetch recent posts from:
βββ User A's post list
βββ User C's post list
βββ User D's post list
β
βΌ
Merge + Rank βββΊ Return to User B
Read time: O(following_count) β computed at request time
Write time: O(1) β just write the post once
π― Key Principle: The choice between push and pull is fundamentally a question of where you want to pay the cost β at write time or at read time β and that decision hinges on the ratio of reads to writes and the distribution of follower counts.
So when do you choose each? Write-time fan-out works beautifully for regular users with, say, hundreds or a few thousand followers. When Kenji posts a photo and has 800 followers, pushing to 800 feed caches is fast and manageable. His followers get an instant, pre-built feed experience.
However, write-time fan-out breaks catastrophically for celebrity accounts. If Taylor Swift tweets something, you'd need to write to 90 million feed caches β a single write event becomes an avalanche that can overwhelm your infrastructure for minutes.
β οΈ Common Mistake: Designing a pure write-time or pure read-time fan-out for a system that has both regular users and celebrities. Real platforms like Twitter and Instagram use a hybrid approach: push for regular users, pull for celebrities (often called "hot" accounts above a follower threshold). When you're building a user's feed at read time, you merge the pre-built feed with a small, on-demand pull from the celebrity accounts that user follows.
π§ Mnemonic: Think "Push for People, Pull for Power-users" β regular people get pushed to, power-users (celebrities) get pulled from.
Event-Driven Architecture: Decoupling at Scale
Once you understand fan-out, you immediately see a problem: the post service shouldn't be responsible for finding all followers, writing to all their feeds, triggering notifications, updating analytics, and refreshing search indexes all in one synchronous call. That's a recipe for a slow, fragile system where one failing downstream component blocks the entire write path.
Event-driven architecture solves this by introducing a message broker β a durable, distributed queue that sits between the event producer (the service that writes the post) and all the consumers that need to react to it. The post service simply publishes an event β "user:123 created post:456" β and its job is done. Every downstream system subscribes independently.
Event-Driven Architecture with Message Queue
[Post Service] ββpublishesβββΊ [Message Broker (Kafka / SQS)]
β
βββββββββββββββββββββββΌββββββββββββββββββββββ
βΌ βΌ βΌ
[Fan-Out Worker] [Notification Worker] [Search Indexer]
β β β
Updates feed cache Sends push alerts Updates Elasticsearch
Apache Kafka is the dominant choice for high-throughput social and streaming platforms. Kafka's architecture treats messages as an immutable, ordered log partitioned across brokers. Consumers maintain their own offset (position in the log), which means multiple independent consumer groups can read the same events without interfering with each other. The fan-out worker and the notification worker both read the same post-created topic independently and at their own pace.
Amazon SQS is a simpler, managed alternative that works beautifully when you're operating on AWS and don't need Kafka's log-replay semantics. SQS is a point-to-point queue β once a message is consumed and acknowledged, it's gone β making it better suited for task queues than for event logs.
π‘ Real-World Example: When you "like" a post on Instagram, that interaction triggers an event. The notification service consumes it to send a push notification to the poster. The analytics pipeline consumes it to update engagement metrics. The recommendation engine consumes it to refine what content to show you next. None of these systems talk to each other β they all just listen to the same event stream.
π― Key Principle: Message queues provide temporal decoupling β the producer and consumer don't need to be available simultaneously β and resilience through buffering, allowing consumers to fall behind during traffic spikes without dropping events.
Microservices Decomposition: Drawing the Right Boundaries
With the communication mechanism established, we can think about how to split the system into services. A common mistake is decomposing by technical layer ("the database service," "the cache service") rather than by business domain. Domain-driven decomposition gives you services that can evolve, scale, and be owned independently.
For a social platform, four services form the backbone:
| Service | Responsibility | Primary Datastore |
|---|---|---|
| π§ User Service | Identity, authentication, social graph (follows/friends) | PostgreSQL + Redis |
| π Content Service | Post/video creation, storage metadata, content retrieval | Cassandra + Object Store |
| π° Feed Service | Feed generation, ranking, caching per-user timelines | Redis + Cassandra |
| π Notification Service | Push/email/SMS alerts, delivery tracking | Kafka + DynamoDB |
The User Service owns the social graph β who follows whom. This data is highly relational by nature (a user has followers, a user follows others, mutual connections matter), which is why a relational database like PostgreSQL is a common starting point. But at massive scale, querying "give me all 80 million followers of this account" from a SQL follows table becomes prohibitively expensive, which is why platforms like Facebook and LinkedIn built dedicated graph databases (TAO, LinkedIn's social graph service) for this purpose.
The Content Service is responsible for accepting new posts and serving them. It doesn't care about your feed β it's the source of truth for content. When the fan-out worker needs the actual content to put in a feed, it calls the Content Service or reads from its store directly.
The Feed Service is where fan-out logic lives. It consumes events from Kafka, executes the push/pull/hybrid strategy described earlier, and maintains per-user feed caches in Redis. When you open your app and your feed loads in 50ms, you're reading a pre-built, Redis-cached list that the Feed Service has been maintaining in the background.
β οΈ Common Mistake: Having the Feed Service own the canonical post data. The feed should store post IDs (pointers), not the full content. This way, if a post is deleted or edited, you don't need to retroactively update millions of feed caches β you just check the Content Service at read time and filter out deleted posts.
Data Modeling Trade-Offs: Choosing the Right Database
The database decision is where many candidates go wrong, either defaulting reflexively to "just use Postgres" or over-engineering with exotic stores. Let's reason through the actual trade-offs.
Relational databases (PostgreSQL, MySQL) shine when your data has complex relationships, you need ACID transactions, and your query patterns are flexible. For a social platform, they're the right choice for the users table, authentication tokens, and billing. They struggle when you're writing millions of rows per second or when a single entity (a user's post list) can have hundreds of thousands of entries that must be retrieved quickly.
Wide-column stores (Apache Cassandra, DynamoDB) are optimized for write-heavy, high-throughput workloads with predictable access patterns. Cassandra's data model organizes data into partition keys and clustering columns. The classic social media pattern:
Cassandra Table: user_posts
Partition Key: user_id
Clustering Column: created_at DESC, post_id
user_id | created_at | post_id | content
--------|---------------------|---------|--------
u123 | 2024-01-15 10:00:00 | p789 | "..."
u123 | 2024-01-14 08:30:00 | p755 | "..."
u456 | 2024-01-15 09:45:00 | p790 | "..."
This design lets you fetch the 20 most recent posts for a user with a single, fast partition scan. Cassandra distributes partitions across nodes automatically, so as your user base grows, you add nodes and the data rebalances. The trade-off: Cassandra has no joins, limited aggregation support, and requires you to model your data around your queries rather than around your entities.
Document stores (MongoDB) sit in the middle β more flexible schema than relational databases, better query capabilities than Cassandra, but with different scaling characteristics. They're a good fit for content metadata where the schema evolves (a video post has different fields than a text post) and you need rich querying.
π‘ Mental Model: Ask yourself three questions about each piece of data:
- How is it written? (single row vs. bulk, transactional vs. fire-and-forget)
- How is it read? (single lookup, range scan, complex join)
- How does it grow? (bounded or unbounded, hot partitions?)
Those answers point you to the right store.
Code Example: Async Event Publishing with a Producer-Consumer Pattern
Let's make these concepts concrete with code. The following Python example simulates the core event-publishing flow that happens when a user creates a post. We use asyncio to represent the non-blocking nature of modern microservice calls, and a simple in-memory queue to stand in for Kafka.
import asyncio
import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from typing import Callable, Dict, List
## ---------------------------------------------------------------------------
## Event schema: what gets published to the message broker
## ---------------------------------------------------------------------------
@dataclass
class PostCreatedEvent:
event_id: str = field(default_factory=lambda: str(uuid.uuid4()))
user_id: str = ""
post_id: str = ""
content: str = ""
timestamp: str = field(default_factory=lambda: datetime.utcnow().isoformat())
def to_json(self) -> str:
return json.dumps(self.__dict__)
## ---------------------------------------------------------------------------
## Simple in-memory broker (stands in for Kafka topic)
## ---------------------------------------------------------------------------
class InMemoryBroker:
def __init__(self):
self._queues: Dict[str, asyncio.Queue] = {}
self._subscribers: Dict[str, List[Callable]] = {}
def subscribe(self, topic: str, handler: Callable):
"""Register a consumer handler for a topic."""
if topic not in self._subscribers:
self._subscribers[topic] = []
self._queues[topic] = asyncio.Queue()
self._subscribers[topic].append(handler)
print(f"[Broker] '{handler.__name__}' subscribed to topic '{topic}'")
async def publish(self, topic: str, event: str):
"""Non-blocking publish β producer returns immediately."""
if topic in self._queues:
await self._queues[topic].put(event)
print(f"[Broker] Event published to '{topic}'")
async def dispatch(self, topic: str):
"""Continuously dispatch events to all subscribers (simulates Kafka consumer groups)."""
queue = self._queues.get(topic)
if not queue:
return
while True:
event_json = await queue.get()
for handler in self._subscribers.get(topic, []):
# Each handler runs independently β decoupled consumers
asyncio.create_task(handler(event_json))
queue.task_done()
## ---------------------------------------------------------------------------
## Producer: the Post Service
## ---------------------------------------------------------------------------
class PostService:
def __init__(self, broker: InMemoryBroker):
self.broker = broker
async def create_post(self, user_id: str, content: str) -> str:
post_id = str(uuid.uuid4())[:8]
event = PostCreatedEvent(
user_id=user_id,
post_id=post_id,
content=content
)
# Publish and move on β no waiting for downstream systems
await self.broker.publish("post-created", event.to_json())
print(f"[PostService] Post {post_id} created for user {user_id}")
return post_id
## ---------------------------------------------------------------------------
## Consumers: Fan-Out Worker and Notification Worker
## ---------------------------------------------------------------------------
async def fan_out_worker(event_json: str):
"""Simulates pushing the post to follower feed caches."""
event = json.loads(event_json)
# In production: fetch followers, write post_id to each user's Redis feed list
await asyncio.sleep(0.05) # Simulate async I/O to Redis
print(f"[FanOutWorker] Fanned out post {event['post_id']} to followers of {event['user_id']}")
async def notification_worker(event_json: str):
"""Simulates sending push notifications to followers."""
event = json.loads(event_json)
# In production: look up followers with notifications enabled, send via APNs/FCM
await asyncio.sleep(0.03) # Simulate async I/O to push service
print(f"[NotificationWorker] Notifications sent for post {event['post_id']}")
## ---------------------------------------------------------------------------
## Wiring it all together
## ---------------------------------------------------------------------------
async def main():
broker = InMemoryBroker()
# Register consumers before any events are produced
broker.subscribe("post-created", fan_out_worker)
broker.subscribe("post-created", notification_worker)
# Start dispatching in the background
asyncio.create_task(broker.dispatch("post-created"))
# Producer: simulate two users creating posts
post_svc = PostService(broker)
await post_svc.create_post("user_alice", "Just hiked to the summit! ποΈ")
await post_svc.create_post("user_bob", "New blog post is live!")
# Allow background tasks to complete
await asyncio.sleep(0.2)
asyncio.run(main())
When you run this code, you'll see the PostService publish its event and immediately return. The broker then dispatches that event concurrently to both the fan_out_worker and the notification_worker, each processing it independently. This mirrors exactly how Kafka consumer groups work β multiple consumers can read from the same topic without coordinating with each other or with the producer.
π‘ Pro Tip: In an interview, you don't need to write production-quality Kafka configuration. What matters is demonstrating that you understand the decoupling contract: the producer publishes a well-defined event schema and doesn't know or care who consumes it. This is what makes the system extensible β adding a new consumer (say, a search indexer) requires zero changes to the Post Service.
Let's also look at a simplified Cassandra-style query pattern to illustrate how the feed service would write and read post references:
## Simulated feed cache operations (in production this would be Redis LPUSH / LRANGE)
## This illustrates the pointer-based fan-out model
from collections import defaultdict
from typing import List, Tuple
import time
class FeedCache:
"""
Simulates a per-user feed cache storing (timestamp, post_id) tuples.
In production: Redis sorted set with score=timestamp, member=post_id
"""
def __init__(self, max_feed_size: int = 800):
self._feeds: dict = defaultdict(list)
self.max_feed_size = max_feed_size
def push_to_feed(self, follower_id: str, post_id: str, author_id: str):
"""Fan-out: add a post reference to a follower's cached feed."""
entry = {
"post_id": post_id,
"author_id": author_id,
"score": time.time() # Used for chronological or ranked ordering
}
feed = self._feeds[follower_id]
feed.append(entry)
# Sort descending by score (most recent first)
feed.sort(key=lambda x: x["score"], reverse=True)
# Trim to max size β we don't keep infinite history in cache
if len(feed) > self.max_feed_size:
self._feeds[follower_id] = feed[:self.max_feed_size]
def get_feed(self, user_id: str, limit: int = 20) -> List[dict]:
"""Read path: return post_id pointers, not full content."""
return self._feeds[user_id][:limit]
## Simulate the fan-out worker writing to feed caches
cache = FeedCache()
followers_of_alice = ["user_bob", "user_carol", "user_dave"]
for follower in followers_of_alice:
cache.push_to_feed(follower, post_id="post_abc123", author_id="user_alice")
## Simulate reading Bob's feed β returns IDs, not content
bobs_feed = cache.get_feed("user_bob")
print("Bob's feed (post IDs only):")
for entry in bobs_feed:
print(f" post_id={entry['post_id']} by {entry['author_id']}")
# In production: content would be hydrated by calling Content Service
Notice that the feed cache stores only post IDs and author IDs β never the full text or media URL. When the client fetches Bob's feed, the Feed Service returns these IDs, and the client or an API gateway makes a batched call to the Content Service to hydrate the actual content. This separation is crucial: if Alice deletes her post, you only need to remove it from one place (the Content Service), not from millions of feed caches.
Putting It All Together: The Architecture in Motion
Let's trace a single user action β Alice follows Bob β through the complete architecture to see how these patterns interlock:
Alice taps "Follow" on Bob's profile
1. API Gateway βββΊ User Service
- Writes follow relationship to PostgreSQL
- Publishes "user:alice followed user:bob" to Kafka topic: user-actions
2. Kafka: user-actions topic
βββ Fan-Out Worker consumes event
β - Fetches Bob's recent posts from Content Service
β - Pushes Bob's post IDs into Alice's feed cache (Redis)
βββ Notification Worker consumes event
- Sends push notification to Bob: "Alice started following you"
3. Alice opens her feed 2 seconds later
- Feed Service reads Alice's pre-built cache from Redis
- Feed is already populated with Bob's recent posts
- Feed loads in ~40ms β
π€ Did you know? At Instagram's scale (500 million daily active users), the fan-out system processes tens of thousands of follow events per second. Each follow event can trigger inserting post IDs into hundreds or thousands of feed caches. The system is designed so that individual feed insertions are so fast (a Redis LPUSH is microseconds) that even large fan-out operations complete within seconds.
The architectural patterns in this section β fan-out strategy, event-driven decoupling, service decomposition, and database selection β aren't independent choices. They form an interconnected system where each decision constrains and enables the others. The fan-out pattern you choose determines your Kafka topic structure. Your database selection shapes your service boundaries. Your service decomposition determines where transactions are possible and where eventual consistency is the only option.
π Quick Reference Card:
| π§ Pattern | β Use When | β οΈ Avoid When |
|---|---|---|
| π Write-time fan-out | Most users have < 10K followers | System has many celebrity accounts |
| π Read-time fan-out | Simple systems, low read traffic | Feed reads are frequent and latency-sensitive |
| π Hybrid fan-out | Mixed follower distributions | You can't define a follower threshold |
| π¨ Kafka | High-throughput, event replay needed | Simple task queuing on AWS |
| ποΈ Cassandra | Write-heavy, known query patterns | You need joins or ad-hoc queries |
| π PostgreSQL | Relational data, ACID transactions | Millions of writes per second |
With these patterns as your foundation, you're equipped to reason about the design decisions that come next: how content itself β images, video, raw bytes β flows through the system from creation to global delivery.
Content Delivery, Storage, and the Media Pipeline
Every time a user posts a video to Instagram, uploads a thumbnail to YouTube, or sends a photo in a WhatsApp message, an invisible assembly line kicks into motion. Raw bytes travel from a mobile device, get transformed, replicated, and ultimately served to millions of viewers β sometimes within seconds. Understanding this pipeline deeply is one of the clearest signals an interviewer uses to separate candidates who have thought seriously about production systems from those who are hand-waving at scale. This section builds that pipeline from the ground up.
The Media Ingestion Pipeline
The journey of a media file begins with ingestion β the process of accepting raw content from a client, validating it, and handing it off to a processing pipeline. This sounds simple, but at scale it becomes one of the most failure-prone parts of the entire system.
The first design decision is how the client sends the file. For small images (under a few megabytes), a single HTTP POST with multipart form data is fine. For video files that can easily exceed a gigabyte, you need chunked transfers β the file is split into fixed-size pieces (commonly 5β10 MB each) and uploaded in parallel or sequentially. Each chunk is assigned a unique identifier and a byte offset. If the connection drops, the client can resume from the last successful chunk rather than starting over. This is the foundation of protocols like TUS (resumable uploads) and the approach taken by YouTube's resumable upload API.
Once chunks arrive at an upload endpoint (a lightweight, stateless service whose only job is accepting bytes), they get assembled and written to a staging area β typically object storage β before any further processing begins. A critical mistake here is doing any heavy work synchronously on the upload path. The upload service should do the minimum: validate file size, check MIME type, authenticate the request, and then acknowledge success to the client. Everything else goes into a queue.
## Simplified upload endpoint handler (Python/Flask style)
import uuid
import boto3
from flask import Flask, request, jsonify
from message_queue import enqueue_transcoding_job
app = Flask(__name__)
s3 = boto3.client('s3')
@app.route('/upload', methods=['POST'])
def upload_media():
file = request.files.get('media')
user_id = request.headers.get('X-User-Id')
# 1. Basic validation β fast, synchronous
if not file or file.content_length > 2 * 1024 * 1024 * 1024: # 2 GB limit
return jsonify({'error': 'Invalid file'}), 400
allowed_types = {'video/mp4', 'image/jpeg', 'image/png', 'image/webp'}
if file.mimetype not in allowed_types:
return jsonify({'error': 'Unsupported media type'}), 415
# 2. Write raw bytes to staging bucket β the ONLY heavy I/O here
media_id = str(uuid.uuid4())
staging_key = f"staging/{user_id}/{media_id}/original"
s3.upload_fileobj(file, 'media-staging-bucket', staging_key)
# 3. Enqueue async processing β do NOT transcode here
enqueue_transcoding_job({
'media_id': media_id,
'user_id': user_id,
'staging_key': staging_key,
'media_type': file.mimetype
})
# 4. Return immediately β client doesn't wait for transcoding
return jsonify({
'media_id': media_id,
'status': 'processing'
}), 202
The 202 Accepted response is intentional: we're telling the client the upload succeeded but processing isn't done yet. The client polls a status endpoint (or receives a webhook/push notification) to know when the media is ready.
Background Transcoding: The Async Pipeline
Once the raw file lands in staging storage, transcoding workers pick up the job from the queue. For video, transcoding means converting the original format into multiple resolution/bitrate variants β a process called creating an adaptive bitrate (ABR) ladder. A typical ladder looks like: 240p, 360p, 480p, 720p, 1080p, and 4K if appropriate. Each variant is also segmented into short chunks (2β10 seconds each) in formats like HLS (HTTP Live Streaming) or MPEG-DASH, which allow video players to dynamically switch quality based on available bandwidth.
For images, transcoding means format conversion (e.g., JPEG β WebP for better compression), resizing to standard thumbnail dimensions, and stripping EXIF metadata for privacy.
MEDIA INGESTION PIPELINE
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CLIENT
βββββββ Chunked Upload ββββββββββββββββββββ
β App ββββββββββββββββββββββββββΊ Upload Service β
βββββββ β (Validate + Auth) β
ββββββββββ¬ββββββββββ
β Write raw file
βΌ
ββββββββββββββββββββ
β Staging Bucket β β S3 / GCS
β (raw originals) β
ββββββββββ¬ββββββββββ
β Enqueue job
βΌ
ββββββββββββββββββββ
β Message Queue β β SQS / Kafka
ββββββββββ¬ββββββββββ
β Consume
βΌ
ββββββββββββββββββββββββββββββββββ
β Transcoding Workers β
β ββββββββ ββββββββ ββββββββ β
β β 240p β β 720p β β1080p β β
β ββββ¬ββββ ββββ¬ββββ ββββ¬ββββ β
βββββββΌβββββββββΌβββββββββΌββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββββββββββββββββββββββ
β Production Bucket β β S3
β /videos/{id}/240p/seg*.ts β
β /videos/{id}/720p/seg*.ts β
β /videos/{id}/master.m3u8 β
ββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββ
β Metadata DB Update β
β status: 'ready' β
β cdn_url: 'cdn.example.com' β
ββββββββββββββββββββββββββββββββ
π‘ Real-World Example: YouTube reportedly runs thousands of transcoding jobs in parallel using a fleet of specialized workers. A single 4K video upload can spawn dozens of parallel tasks β different resolutions, audio tracks, subtitle embedding, and thumbnail generation β all coordinated through a distributed job queue.
Object Storage Architecture
Object storage systems like Amazon S3, Google Cloud Storage, and Azure Blob Storage are the backbone of media-heavy systems. Unlike block storage (disks) or file storage (NFS), object storage trades hierarchical structure and low-latency random writes for massive horizontal scalability, durability (S3 promises 11 nines β 99.999999999%), and a simple key-value access model. Every file is identified by a globally unique key and accessed over HTTP.
Bucket Design
Bucket design matters more than most candidates acknowledge. A common pattern is to separate buckets by lifecycle stage and access pattern:
media-stagingβ temporary, receives raw uploads, aggressively cleaned upmedia-productionβ processed, publicly accessible via CDNmedia-archiveβ content older than 90 days, rarely accessed, deep cold storagemedia-backupβ cross-region replication for disaster recovery
Within a production bucket, a meaningful key structure helps with debugging and access control. A prefix like videos/{user_id}/{video_id}/ makes it easy to apply IAM policies at the user level and to list all assets for a given video.
Lifecycle Policies and Cost-Optimized Tiering
Storage costs compound fast when you're hosting petabytes of video. Lifecycle policies are automated rules that transition objects between storage tiers based on age or access frequency. S3 offers several tiers:
| Storage Class | Retrieval Latency | Monthly Cost | Best For |
|---|---|---|---|
| π₯ S3 Standard | Milliseconds | ~$0.023/GB | Active content, recent uploads |
| π‘οΈ S3 Standard-IA | Milliseconds | ~$0.0125/GB | Content accessed < monthly |
| π§ S3 Glacier Instant | Milliseconds | ~$0.004/GB | Archives accessed occasionally |
| π§ S3 Glacier Deep | Hours | ~$0.00099/GB | Regulatory archives, backups |
A real policy might say: transition to Standard-IA after 30 days, Glacier Instant after 90 days, Glacier Deep Archive after 365 days. This alone can reduce storage costs by 60β80% for platforms where most content gets a spike of views at upload and then steadily declines.
π― Key Principle: The access pattern of media content follows a power law β roughly 20% of videos receive 80% of views, and most views happen within 48 hours of upload. Design your storage tiers to match this reality.
CDN Fundamentals: Bringing Content to the Edge
Even with perfectly optimized object storage, serving media directly from a single S3 bucket in us-east-1 to a user in Tokyo would be painfully slow. This is where Content Delivery Networks (CDNs) become indispensable.
A CDN is a globally distributed network of edge servers (also called Points of Presence or PoPs). When a user requests a video segment, the CDN routes that request to the nearest edge server. If the edge server has the file cached, it responds immediately β no trip to the origin. This is a cache hit. If not, it fetches from origin (your S3 bucket), caches it locally, and serves it to the user β a cache miss that populates the edge for future requests.
CDN REQUEST FLOW
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
User (Tokyo) CDN Edge (Tokyo PoP) Origin (S3, US)
ββββββββββββ ββββββββββββββββββ βββββββββββββββ
β β β
β GET /video/seg_001.ts β β
ββββββββββββββββββββββββββΊβ β
β β β
β [CACHE HIT?] β β
β β β
βββββ€ 200 OK (5ms) β No origin hit! β
β (served from edge) β β
β β β
β β β β β β β β β β β β β βββ β β β β β β β β β β β
β β β
β GET /video/seg_002.ts β [CACHE MISS] β
ββββββββββββββββββββββββββΊβββββββββββββββββββββββββΊβ
β ββββββββββββββββββββββββββ€
β 200 OK (180ms) β (fetched + cached) β
βββββ€ (edge β origin) β β
Cache Invalidation Strategies
Cache invalidation β removing or updating stale cached content β is notoriously difficult. The main strategies are:
- π§ TTL-based expiration: Set a
Cache-Control: max-age=31536000header for immutable content (video segments never change once created). This is the most efficient approach and should be your default for processed media. - π§ Versioned URLs: Instead of invalidating, change the URL.
/thumbnail/v3/abc123.webpis a new cache entry. The old version expires on its own TTL. This works beautifully for thumbnails that get updated. - π§ Purge API calls: CDNs like Cloudflare and CloudFront expose APIs to explicitly evict keys. Reserve this for emergencies (DMCA takedowns, content moderation) β purge calls at scale are expensive and slow.
π‘ Mental Model: Treat immutable content (video segments, compressed images) as write-once with infinite TTLs. Treat mutable content (thumbnails, profile pictures) as versioned resources. This eliminates 95% of invalidation headaches.
Geographic Routing
CDNs route requests using anycast routing or GeoDNS. When a user's DNS lookup resolves a CDN hostname, the DNS server returns the IP address of the nearest PoP rather than a fixed server. This means cdn.example.com might resolve to a server in Singapore for a user in Jakarta and a server in Frankfurt for a user in Berlin β entirely transparent to the application layer.
Blob Metadata vs. Blob Content Separation
One of the most important architectural principles in media system design is the strict separation of blob metadata from blob content. This is a specific application of the broader principle of separating structured, queryable data from unstructured binary data.
The pattern looks like this: your fast relational or NoSQL database stores a row describing the media β its ID, owner, dimensions, duration, status, and critically, its CDN URL. The actual bytes live in object storage and are never touched by your application server during reads.
-- What lives in your fast database (PostgreSQL, DynamoDB, etc.)
CREATE TABLE media_assets (
media_id UUID PRIMARY KEY,
user_id UUID NOT NULL,
media_type VARCHAR(20) NOT NULL, -- 'video', 'image'
status VARCHAR(20) NOT NULL, -- 'processing', 'ready', 'failed'
duration_secs INT, -- NULL until transcoding complete
width_px INT,
height_px INT,
file_size_bytes BIGINT,
-- URLs pointing to CDN/object storage β NOT the bytes themselves
cdn_url_720p TEXT, -- 'https://cdn.example.com/v/abc123/720p/master.m3u8'
cdn_url_1080p TEXT,
thumbnail_url TEXT, -- 'https://cdn.example.com/img/abc123/thumb_v1.webp'
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- What does NOT live in your database
-- β No BLOB columns
-- β No binary data
-- β No base64-encoded content
When a client requests a video, your API service queries the database for the row, retrieves the cdn_url_720p, and returns it to the client. The client then talks directly to the CDN β your API servers are completely bypassed for the actual media transfer. This is the decoupling that makes the system scale.
SEPARATED METADATA + CONTENT ARCHITECTURE
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CLIENT API SERVER CDN / Object Storage
ββββββ ββββββββββ ββββββββββββββββββββ
β β β
β GET /api/video/123 β β
ββββββββββββββββββββββββΊβ β
β β SELECT cdn_url β
β βββββ (fast DB query) ββββΊβ
β βββββββββββββββββββββββββββ
β β β
β 200 OK β β
β { cdn_url: '...' } β β
βββββββββββββββββββββββββ β
β β
β GET cdn.example.com/video/123/720p/master.m3u8 β
ββββββββββββββββββββββββββββββββββββββββββββββββββΊβ
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β (video bytes, served directly from edge) β
The API server handles exactly two small, fast operations: authenticate the request and return a URL string. It never touches video bytes. This means your API fleet stays small and your database stays fast, regardless of how much video traffic you're serving.
π€ Did you know? Netflix's architecture has API servers return playback manifests β essentially structured lists of CDN URLs for video segments β and the player then fetches segments directly from edge servers. A typical Netflix API response for starting a video might be a few kilobytes of JSON; the video itself is terabytes served entirely from the CDN edge.
β οΈ The Most Dangerous Pitfall: Serving Media from Application Servers
Now that we've established the right architecture, let's examine what happens when you get it wrong β because this exact mistake appears in interviews constantly, and it's often the thing that causes real production outages.
β οΈ Common Mistake: Routing media uploads and downloads through application servers.
β Wrong thinking: "I'll have my Express/Django/Rails server accept uploads and serve files directly. It's simpler and I can add authentication in middleware."
β Correct thinking: "My application servers should only ever handle metadata. All binary data flows through dedicated upload endpoints β object storage β CDN."
Here's why serving media from application servers creates cascading failures:
The Cascade Failure Pattern
Memory exhaustion: A typical HTTP request holds data in memory while processing. Streaming a 500 MB video file through an application server uses 500 MB of memory for that single connection β for the entire duration of the stream. With 100 concurrent video viewers, you've consumed 50 GB of memory. Node.js, even with streaming APIs, will exhaust its heap and crash.
Thread/process starvation: Synchronous frameworks block a thread per request. Long-lived video connections hold those threads indefinitely, starving new API requests β login, comment posting, search β of the processing capacity they need.
Bandwidth exhaustion at origin: If your application servers are co-located with your API services, serving video will saturate the network interface, causing API timeouts for every request hitting that machine.
No geographic distribution: Application servers are in one region. Users in distant regions experience high latency for every segment of every video, regardless of how often that segment has been watched.
Auto-scaling becomes ineffective: You'll find yourself scaling your entire application server fleet β with all the startup time, configuration, and cost that entails β just to serve bytes that a CDN would have served for fractions of a cent without any operational overhead.
WRONG: MEDIA THROUGH APP SERVERS
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1000 video viewers βββΊ App Server Fleet βββΊ DB
(memory exhausted,
threads blocked,
network saturated)
β
API requests FAIL
Login FAILS
Search FAILS
π₯ Full outage
RIGHT: MEDIA BYPASSES APP SERVERS
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1000 video viewers βββΊ CDN Edge βββΊ (S3 on cache miss only)
(stateless, infinite scale)
β
API servers handle ONLY metadata queries
(tiny requests, fast responses, stable)
β
System remains healthy
π‘ Pro Tip: When an interviewer asks about handling a sudden viral video β say, a clip that goes from 0 to 10 million views in an hour β the correct answer always involves the CDN absorbing the traffic spike entirely. Origin servers (S3) see a fraction of requests because the CDN edge has the content cached. Your API servers see a manageable number of metadata requests. This is the architecture that makes "going viral" a success story rather than an outage.
Putting It All Together
π Quick Reference Card: Media Pipeline Design Decisions
| Decision Point | β Correct Pattern | β Anti-Pattern |
|---|---|---|
| π§ Upload handling | Chunked, async, enqueue jobs | Synchronous transcoding on upload |
| π¦ File storage | Object storage (S3/GCS) | Local disk, database BLOBs |
| ποΈ Metadata | Fast DB stores URLs only | DB stores binary content |
| π Content delivery | CDN with edge caching | App servers serve bytes |
| π° Storage cost | Lifecycle tiering (hotβcold) | One tier for all content |
| π Cache strategy | Versioned URLs + long TTLs | Frequent purge API calls |
| π‘ Geographic reach | CDN anycast/GeoDNS | Single-region origin serving |
The media pipeline is one of those system design topics where the details compound in interesting ways. The decision to separate metadata from content enables the CDN architecture. The CDN architecture enables the viral traffic pattern. The chunked upload approach enables large file support and resumability. Each choice reinforces the others, and a good interviewer will probe each layer to see if you understand why the pieces fit together β not just that they do.
π§ Mnemonic: STUD β Stage it first, Transcode asynchronously, URL in the database (not bytes), Deliver via CDN. Every media file you design a system for should go through STUD.
Scalability Fundamentals: Caching, Sharding, and Rate Limiting
Every system design interview for a social or streaming platform eventually arrives at the same inflection point: your interviewer nods politely at your service decomposition, then leans forward and asks, "So how does this actually scale to 500 million users?" This is the moment that separates candidates who understand distributed systems in theory from those who have internalized the practical techniques that make production systems work. Caching, sharding, and rate limiting are not optional optimizations you bolt on at the end β they are load-bearing pillars of any serious architecture. Mastering them, and knowing when and why to apply each, is what transforms a whiteboard sketch into a credible design.
Caching Strategies: Reading Fast in a World That Writes Slowly
The fundamental insight behind caching is brutally simple: most data is read far more often than it is written. On a social platform like Twitter or Instagram, a single celebrity post might be written once and read 50 million times in the next hour. Without caching, every one of those reads would hammer your database β a system designed to handle writes carefully, with transactions and disk persistence, not to serve millions of reads per second.
Cache-aside (also called lazy loading) is the most common pattern you will reach for. The application first checks the cache; on a miss, it fetches from the database, populates the cache, and returns the result. Future reads hit the cache until the entry expires or is evicted.
APPLICATION LAYER
β
βΌ
βββββββββββββββββ HIT βββββββββββββββββ
β Check Cache ββββββββββΊβ Return cached β
β (Redis) β β data β β
βββββββββββββββββ βββββββββββββββββ
β MISS
βΌ
βββββββββββββββββ βββββββββββββββββ
β Query DB ββββββββββΊβ Write result β
β (Postgres) β β to Cache β
βββββββββββββββββ βββββββββββββββββ
For a social feed, you might cache a user's pre-computed timeline under the key feed:{user_id} with a TTL of 60 seconds. The trade-off is cache staleness β a user might not see a new post for up to a minute. For most social contexts this is perfectly acceptable. The cache-aside pattern also gracefully handles cache failures: if Redis goes down, the application simply reads from the database every time, degrading performance but not correctness.
Write-through caching inverts the relationship: every write goes to the cache and the database simultaneously before the operation is confirmed. This guarantees that cached data is never stale, but it means every write pays a double-write penalty. This pattern shines for session data and user profile information that must be consistent β you never want a user to log in and see yesterday's display name because the cache hadn't been updated.
Read-through caching places the cache in front of the database as a transparent proxy. The application only ever talks to the cache layer; on a miss, the cache itself fetches from the database and populates itself. This pattern is less common in hand-rolled systems but appears frequently in managed services like Amazon ElastiCache with read-through enabled. It simplifies application code at the cost of making the cache a harder dependency.
## Cache-aside pattern with Redis for a social media user profile
import redis
import json
from typing import Optional
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def get_user_profile(user_id: int) -> dict:
cache_key = f"user:profile:{user_id}"
# Step 1: Try the cache first
cached = r.get(cache_key)
if cached:
return json.loads(cached) # Cache HIT β fast path
# Step 2: Cache MISS β go to database
profile = db.query("SELECT * FROM users WHERE id = %s", user_id)
# Step 3: Populate cache with a 5-minute TTL
# TTL prevents stale data from living forever
r.setex(cache_key, 300, json.dumps(profile))
return profile
def update_user_profile(user_id: int, data: dict) -> None:
# Write-through: update DB first, then invalidate/update cache
db.execute("UPDATE users SET ... WHERE id = %s", user_id)
cache_key = f"user:profile:{user_id}"
# Invalidate rather than update β simpler and avoids race conditions
r.delete(cache_key)
β οΈ Common Mistake: Caching computed timelines without a sensible invalidation strategy. If you cache feed:{user_id} but never invalidate it when someone the user follows posts new content, your feed becomes a museum exhibit. A practical solution is fan-out on write: when a user posts, push references to that post into the cached feeds of all their followers. Twitter calls this a push model. For users with millions of followers (celebrities), you often switch to a pull model β compute their contributions at read time β to avoid the thundering herd of millions of cache writes.
π‘ Real-World Example: Netflix caches its homepage recommendations aggressively in a tiered cache. The edge layer (CDN) caches the general structure; a regional cache holds personalized thumbnails; and a local in-process cache holds the most recently accessed data. Each layer has progressively shorter TTLs as you get closer to the user.
Database Sharding: Dividing the Kingdom
Even the most aggressively cached system eventually needs to write data somewhere, and a single database node has hard limits β both in storage and in write throughput. Sharding (also called horizontal partitioning) splits your data across multiple database instances, called shards, so that each shard owns a subset of the total dataset and handles only a fraction of the total traffic.
The critical design decision is your sharding key β the attribute you use to determine which shard a given record belongs to. Choose wrong and you create hotspots; choose right and load distributes evenly across your fleet.
Hash-Based vs. Range-Based Sharding
Hash-based sharding applies a hash function to the sharding key and uses the result (modulo the number of shards) to determine placement. If you have 8 shards and user ID 12345, you compute hash(12345) % 8 = 3 and route to shard 3. This distributes load extremely evenly because hash functions spread values uniformly β no shard ends up with all the popular users.
Range-based sharding assigns contiguous ranges of key values to each shard. Shard 1 holds user IDs 1β10M, shard 2 holds 10Mβ20M, and so on. This is excellent for range queries β fetching all posts between two timestamps, for example, can be served by a single shard. The danger is hotspots: if most active users have recently created accounts (high IDs), shard 4 gets hammered while shards 1-3 are idle.
HASH-BASED SHARDING (even distribution)
User IDs β hash(id) % 4 β Shard assignment
ID: 1001 β hash β Shard 2 ββββββββββ balanced
ID: 1002 β hash β Shard 0 ββββββββββ load
ID: 1003 β hash β Shard 3 ββββββββββ across
ID: 1004 β hash β Shard 1 ββββββββββ all shards
RANGE-BASED SHARDING (potential hotspot)
ID range Shard Load
0-25M β Shard 0 βββ (old, less active users)
25-50M β Shard 1 βββββ
50-75M β Shard 2 ββββββββ
75-100M β Shard 3 ββββββββββββββββ β HOTSPOT (new users)
The deeper problem with naive hash-based sharding is rebalancing. If you start with 4 shards and need to add a 5th, hash(id) % 5 now produces completely different assignments for most keys. Migrating data is an enormous operational undertaking. This is where consistent hashing enters the picture.
Consistent hashing places both servers and keys on a conceptual ring of hash values (0 to 2Β³Β²). Each key is assigned to the first server clockwise from its position on the ring. When you add or remove a server, only the keys between the new server and its predecessor need to be remapped β roughly 1/n of all keys, where n is the number of servers. This makes scaling incremental and manageable. Real systems like Cassandra and Amazon DynamoDB are built on this principle.
CONSISTENT HASHING RING
0
_____|_____
/ \
270 | Node A | 90
| (shard1) |
\___________/
Node D Node B
(shard4) (shard2)
Node C
(shard3)
180
Adding Node E between A and B:
β Only keys in that arc migrate
β All other keys stay put β
π― Key Principle: For user data on a social platform, shard on user_id. For content (posts, videos), shard on content_id. Never shard on a low-cardinality attribute like country or account_type β you'll end up with 3 active shards out of 50.
π‘ Mental Model: Think of sharding like assigning customers to bank branches. Hash-based sharding is like alphabetical assignment β perfectly even, but you have to visit the right branch for any transaction. Range-based sharding is like assigning by neighborhood β efficient for local queries, but the downtown branch serving new high-rises gets overwhelmed.
Rate Limiting: Teaching Systems to Say No
A social or streaming system that accepts unlimited requests from any client is not a system β it's an invitation to be taken offline. Rate limiting enforces a maximum request rate per user, IP address, or API key, protecting your infrastructure from abuse, accidental traffic spikes, and denial-of-service attacks.
Three algorithms dominate interview discussions and production systems:
Token bucket maintains a bucket with a maximum capacity of N tokens. Tokens are added at a fixed rate (say, 10 per second). Each request consumes one token; if the bucket is empty, the request is rejected. The bucket allows bursting β a user who hasn't made requests for 30 seconds has a full bucket and can fire 30 requests instantly. This matches real user behavior well.
Leaky bucket models a bucket with a hole at the bottom. Requests fill the bucket from the top; they're processed at a fixed rate from the hole. If the bucket overflows, requests are dropped. Unlike the token bucket, the leaky bucket enforces a perfectly smooth output rate β useful for protecting downstream services that can't handle bursts.
Sliding window counter divides time into windows and counts requests within the current window. A pure fixed-window counter has a boundary problem: a user can make N requests at 11:59:59 and N more at 12:00:01 β effectively 2N requests in 2 seconds. The sliding window smooths this by weighting the previous window based on how much of it falls within the current lookback period.
import redis
import time
from typing import Tuple
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
def is_rate_limited(
user_id: str,
max_requests: int = 100,
window_seconds: int = 60
) -> Tuple[bool, int]:
"""
Sliding window rate limiter backed by Redis.
Returns (is_limited: bool, requests_remaining: int)
"""
now = time.time()
window_start = now - window_seconds
key = f"rate_limit:{user_id}"
# Use a Redis sorted set where:
# - Score = timestamp of the request
# - Member = unique request ID (timestamp + random)
pipe = r.pipeline()
# Remove requests outside the current window
pipe.zremrangebyscore(key, 0, window_start)
# Count requests in the current window
pipe.zcard(key)
# Add the current request
request_id = f"{now}:{user_id}"
pipe.zadd(key, {request_id: now})
# Set expiry so Redis doesn't hold stale keys forever
pipe.expire(key, window_seconds * 2)
# Execute atomically
results = pipe.execute()
request_count = results[1] # Count BEFORE adding current request
if request_count >= max_requests:
# Remove the request we just added β it's rejected
r.zrem(key, request_id)
return True, 0 # Rate limited
remaining = max_requests - request_count - 1
return False, remaining # Not limited
## Usage example in a Flask route
def handle_api_request(user_id: str):
limited, remaining = is_rate_limited(user_id, max_requests=100, window_seconds=60)
if limited:
return {"error": "Rate limit exceeded. Try again later."}, 429
# Add rate limit headers (industry best practice)
headers = {
"X-RateLimit-Remaining": str(remaining),
"X-RateLimit-Reset": str(int(time.time()) + 60)
}
return process_request(), 200, headers
This implementation uses a Redis sorted set where each member is a unique request identifier and its score is the Unix timestamp. This gives us O(log N) insertion and O(log N) cleanup of expired requests β efficient enough for thousands of requests per second per user.
β οΈ Common Mistake: Building rate limiting in application memory on a single server. The moment you deploy multiple instances behind a load balancer, each instance has its own counter, and a user can multiply their effective rate limit by the number of servers. Always use a shared, external store like Redis for rate limit counters in distributed systems.
π§ Mnemonic: Token bucket = you earn tokens to spend (rewards patience). Leaky bucket = water drips at a fixed rate (enforces smoothness). Sliding window = looks back in time to stay honest at boundaries.
Horizontal Scaling: Stateless Is Easy, Stateful Is Hard
One of the most important architectural principles for scaling is the distinction between stateless and stateful services. A stateless service holds no session-specific data in memory between requests β every request carries all the information needed to process it, or that information lives in an external store. Stateful services maintain in-memory state that is specific to a connection or session.
Stateless HTTP services β your REST API servers, feed computation workers, search services β scale horizontally with extraordinary ease. You place them behind a load balancer (NGINX, AWS ALB, HAProxy) and simply add more instances when CPU or memory pressure rises. Any instance can handle any request. Rolling deployments, canary releases, and autoscaling are all trivial because no client is "stuck" to a particular instance.
STATELESS SCALING (easy β)
Clients β Load Balancer β [API-1] [API-2] [API-3] [API-N]
β β β β
[Shared DB / Cache / Queue]
Any server can handle any request. Add API-N+1 anytime.
STATEFUL SCALING (hard β οΈ)
Client-A βββββββββββββββββββΊ WebSocket-1 (holds A's connection state)
Client-B βββββββββββββββββββΊ WebSocket-2 (holds B's connection state)
Client-C βββββββββββββββββββΊ WebSocket-1 (holds C's connection state)
If WebSocket-1 crashes, Clients A and C lose their sessions.
If we need to send A a message, we MUST reach WebSocket-1.
WebSocket servers for real-time notifications and live streaming are the canonical stateful scaling challenge. A WebSocket connection is a persistent, bidirectional TCP connection between a specific client and a specific server. When a new message arrives for User A, your system must route it to the exact server holding User A's WebSocket connection β not just any server in the fleet.
The standard solution is a pub/sub message broker (Redis Pub/Sub, Kafka, or NATS) as a routing layer. When User A connects to WebSocket-Server-1, that server subscribes to a channel user:A:messages. When any service needs to deliver a message to User A, it publishes to that channel. Redis broadcasts it to all subscribers β which is exactly WebSocket-Server-1 β and the message reaches User A. The WebSocket servers themselves remain relatively stateless from the message-routing perspective.
Back-of-the-Envelope Estimation: The Language of Scale
No discussion of scalability is complete without addressing back-of-the-envelope estimation β the practice of deriving concrete numbers that justify your architectural choices. Interviewers do not expect precision; they expect order-of-magnitude reasoning that demonstrates you understand the scale of what you're building.
Let's work through a concrete example: estimating the infrastructure requirements for a Twitter-like service.
Assumptions (state these explicitly):
- 300 million monthly active users (MAU)
- 50 million daily active users (DAU)
- Average user reads their feed 5 times per day
- Average user posts 2 tweets per day (top users post more, most post less)
- Average tweet is 300 bytes of text; 20% include a 100KB image
QPS Estimation:
Write QPS:
50M users Γ 2 tweets/day = 100M tweets/day
100M / 86,400 seconds β 1,160 writes/second (peak ~3x = 3,500 write QPS)
Read QPS:
50M users Γ 5 feed reads/day = 250M reads/day
250M / 86,400 β 2,900 reads/second (peak ~10x = 29,000 read QPS)
β Read-to-write ratio β 25:1 (justify heavy caching and read replicas)
Storage Estimation:
Tweet text storage:
100M tweets/day Γ 300 bytes = 30 GB/day
30 GB Γ 365 = ~11 TB/year (manageable on a few database nodes)
Image storage:
100M tweets Γ 20% with images Γ 100 KB = 2,000 GB = 2 TB/day
2 TB Γ 365 = ~730 TB/year β requires dedicated object storage (S3)
Bandwidth Estimation:
Inbound (writes):
100M tweets/day, 20% with images
Text: 80M Γ 300B = 24 GB/day β 0.3 GB/hour
Images: 20M Γ 100KB = 2,000 GB/day β 23 GB/hour
Outbound (reads):
Assuming each feed load returns 20 tweets:
250M feed loads Γ 20 tweets Γ 300B = 1,500 GB/day text
+ image thumbnails (50KB each): 250M Γ 20 Γ 20% Γ 50KB = 50,000 GB/day
β Images dominate bandwidth β CDN is not optional, it's mandatory
π Quick Reference Card: Estimation Numbers to Memorize
| π’ Quantity | π Value |
|---|---|
| π Seconds in a day | ~86,400 |
| πΎ Characters in a tweet (ASCII) | 1 byte each |
| πΌοΈ Compressed thumbnail | ~50β100 KB |
| π¬ 1 min HD video (compressed) | ~100β200 MB |
| π CDN throughput (large providers) | Tbps range |
| β‘ Redis operations/second | ~100,000β1M |
| ποΈ Postgres reads/second (cached) | ~10,000β50,000 |
π‘ Pro Tip: In an interview, narrate your estimation aloud. Say: "Text storage is manageable β a few terabytes per year is trivially handled by a cluster of Postgres nodes with SSDs. But image and video storage at 730 TB/year immediately tells me I need object storage like S3, and the bandwidth numbers tell me a CDN isn't optional β it's the only way to serve this at acceptable cost and latency." This shows you're using numbers to drive architectural decisions, not just performing arithmetic.
π― Key Principle: Estimation exists to identify which constraints dominate. If your read QPS is 30,000/second and a single Redis instance handles 500,000 operations/second, caching solves your read problem trivially. If your video storage grows at 730 TB/year, you know immediately that self-hosting storage is impractical. The numbers tell you where to focus your architectural energy.
β οΈ Common Mistake: Presenting estimations without drawing conclusions. "We have 30,000 read QPS" is incomplete. "We have 30,000 read QPS, which a single database cannot handle, so we need read replicas plus an aggressive caching layer that should absorb ~90% of reads, reducing effective DB load to ~3,000 QPS which is well within a PostgreSQL cluster's capabilities" β that is what interviewers reward.
The arc from raw estimation to architectural decision is exactly the mental model you want to demonstrate. Caching, sharding, and rate limiting are not techniques you apply because they sound impressive β they are the necessary consequences of the numbers you derive. When you can show an interviewer that you chose consistent hashing for your sharding strategy because your write QPS requires 8 database shards and you need to be able to add a 9th without downtime, you are thinking like an engineer who has built systems at scale, not one who has merely read about them.
Common Mistakes Candidates Make Designing These Systems
Even experienced engineers stumble in system design interviews for social and streaming platforms. The stakes are high: these interviews test not just technical knowledge but judgment, communication, and the ability to reason under uncertainty. The mistakes below are not obscure edge cases β they are the patterns interviewers see repeatedly, and correcting them is often the difference between an offer and a rejection. Let's walk through each one with concrete examples and the thinking patterns that fix them.
Mistake 1: Over-Engineering from the Start β οΈ
The single most common career-limiting move in a system design interview is opening with complexity. A candidate hears "design a social media feed" and immediately launches into CQRS (Command Query Responsibility Segregation), event sourcing, saga patterns, and Kafka topology diagrams β before anyone has agreed on what the system actually needs to do.
This approach signals the opposite of what you intend. Rather than demonstrating mastery, it reveals an inability to distinguish between foundational requirements and optimization strategies. Interviewers want to see that you can start simple and layer complexity only when justified.
β Wrong thinking: "I'll show I'm senior by proposing the most sophisticated architecture possible."
β Correct thinking: "I'll establish requirements, estimate scale, then choose the simplest architecture that meets those constraints β and explain exactly why I'd add complexity if the numbers demanded it."
Consider this progression. For a feed system serving 10,000 daily active users, a monolithic Rails or Django app with a single PostgreSQL instance and server-side rendering is entirely appropriate. You don't need CQRS. You don't need Kafka. You need working software that ships.
Now push that to 10 million DAUs generating 500 million feed reads per day. Now you have a conversation about read replicas, a pre-computed feed cache, and potentially a write fanout service. The architecture evolves because the numbers demand it β not because the engineer wanted to sound impressive.
## WRONG: Immediately proposing an event-sourced feed aggregate
class FeedAggregate:
def __init__(self):
self.events = [] # Every state change is an event β overkill for initial design
def apply_post_created(self, event):
# Replaying from event log to rebuild state
# Complex, hard to debug, unnecessary at early scale
self.events.append(event)
return self._rebuild_state()
## RIGHT: Start with a simple read model that can evolve
class FeedService:
def __init__(self, db, cache):
self.db = db
self.cache = cache
def get_feed(self, user_id: int, limit: int = 20) -> list:
"""
Simple pull model: query posts from followed users.
Fast to implement, correct, and easy to profile.
Upgrade to pre-computed push model only when latency data demands it.
"""
cache_key = f"feed:{user_id}"
cached = self.cache.get(cache_key)
if cached:
return cached
# Straightforward SQL join β profile this first before optimizing
feed = self.db.query("""
SELECT p.* FROM posts p
JOIN follows f ON p.author_id = f.followed_id
WHERE f.follower_id = %s
ORDER BY p.created_at DESC
LIMIT %s
""", (user_id, limit))
self.cache.setex(cache_key, 300, feed) # 5-minute TTL
return feed
The code above illustrates the mindset: the simple version works, is measurable, and has a clear upgrade path. You tell your interviewer: "This pull model works up to about X QPS. Once we hit that threshold, I'd move to pre-computed feeds pushed into Redis β here's how that would work." That is a senior engineering answer.
π‘ Pro Tip: Structure your design in three acts. Act 1: clarify requirements and non-goals. Act 2: design the MVP that works. Act 3: identify bottlenecks and propose targeted upgrades. Interviewers reward this narrative arc.
Mistake 2: Ignoring the Read/Write Ratio β οΈ
Social and streaming systems are profoundly asymmetric workloads. A typical social feed has a read-to-write ratio of roughly 100:1 β for every post written, the feed is read a hundred times. Video streaming systems are even more extreme: Netflix has reported that serving content dwarfs content ingestion by orders of magnitude.
Candidates who design a symmetric architecture β identical resources handling reads and writes β fundamentally misunderstand where the pressure lives in the system.
SYMMETRIC (WRONG for social feeds)
βββββββββββββββ βββββββββββββββ
β App Server ββββββΆβ Database β
β (reads + βββββββ (single) β
β writes) β β β
βββββββββββββββ βββββββββββββββ
All traffic hits the same path.
Write latency spikes hurt reads.
No differentiation in scaling.
ASYMMETRIC (RIGHT for social feeds)
βββββββββββββββββββββββββββββββββββ
β Load Balancer β
βββββββββββββ¬ββββββββββββββ¬βββββββββ
β β
ββββββββββββββββββΌβββ ββββββββΌβββββββββββββ
β Read Service β β Write Service β
β (scaled 10x) β β (scaled 1x) β
βββββββββββ¬ββββββββββ ββββββββ¬βββββββββββββ
β β
ββββββββββββΌβββββββ ββββββββββΌβββββββββββ
β Read Replicas β β Primary DB + β
β + Redis Cache β β Write Queue β
βββββββββββββββββββ βββββββββββββββββββββ
When you design symmetrically, a write spike (a celebrity posts something) can starve read capacity β or worse, a slow analytical query on the write path can block user-facing reads. Separating concerns lets you scale each side independently and protect read SLAs from write variability.
π― Key Principle: Your architecture should reflect your traffic shape. High read-to-write ratio β invest in read replicas, caching layers, CDN, and pre-computation. Low-latency writes matter less than ensuring reads return in under 100ms.
β οΈ Common Mistake: Candidates who do acknowledge the read/write imbalance often stop at "add a cache." That's necessary but insufficient. You also need to discuss cache invalidation strategy, replica lag tolerance, and what happens to reads when the cache is cold β for example, after a deployment flushes Redis.
Mistake 3: Treating the Database as a Single Monolith β οΈ
Novice designers pick one database β usually MySQL or PostgreSQL β and route every data type through it: user profiles, friendship graphs, post content, media metadata, activity streams, search indexes. This works at small scale but becomes a liability as the system grows, because different data types have radically different access patterns.
The corrected approach is polyglot persistence: selecting the right storage engine for each data type based on its access pattern, consistency requirements, and query shape.
| Data Type | Access Pattern | Right Tool | Why |
|---|---|---|---|
| π User profiles | Point lookups by user_id | PostgreSQL / MySQL | ACID, structured, relational |
| π Social graph | Traversal (friends of friends) | Neo4j / Amazon Neptune | Graph traversals are O(1) per hop |
| π° Feed timeline | Range scans by time | Cassandra / DynamoDB | Wide-column, time-ordered writes |
| π Post search | Full-text, fuzzy | Elasticsearch | Inverted index, relevance scoring |
| πΈ Media files | Binary blob, CDN-served | S3 + CloudFront | Object storage isn't a database job |
| β‘ Session/cache | TTL-based key-value | Redis | Sub-millisecond, volatile OK |
Beyond polyglot persistence, candidates frequently forget to mention read replicas. A single primary database handling both analytical queries ("how many posts did this user make last month?") and user-facing queries ("render this profile") will eventually collapse under the mixed load. Read replicas let you route heavy, non-critical queries away from the primary.
## Demonstrating polyglot persistence in a feed service
import redis
import psycopg2
from cassandra.cluster import Cluster
class FeedRepository:
def __init__(self):
# Redis: blazing-fast pre-computed feed cache
self.cache = redis.Redis(host='cache-cluster', decode_responses=True)
# PostgreSQL (READ REPLICA): user metadata lookups
self.pg_read = psycopg2.connect(
host='postgres-read-replica', # NOT the primary!
database='social'
)
# Cassandra: time-series feed storage at scale
# Wide-column layout: partition by user_id, cluster by timestamp
cassandra_cluster = Cluster(['cassandra-node-1', 'cassandra-node-2'])
self.cassandra = cassandra_cluster.connect('social_keyspace')
def get_user_feed(self, user_id: str, page_token: str = None):
# Layer 1: Check Redis cache (sub-millisecond)
cache_key = f"feed_v2:{user_id}"
cached_feed = self.cache.lrange(cache_key, 0, 19) # First 20 items
if cached_feed and not page_token: # Cache only for first page
return {"posts": cached_feed, "source": "cache"}
# Layer 2: Cassandra for persistent feed storage
# Efficient range scan β this is what Cassandra is designed for
query = """
SELECT post_id, content_preview, created_at, author_id
FROM user_feeds
WHERE user_id = %s AND created_at < %s
ORDER BY created_at DESC
LIMIT 20
"""
cursor = page_token or 'now'
rows = self.cassandra.execute(query, (user_id, cursor))
return {"posts": list(rows), "source": "cassandra"}
def enrich_with_author_metadata(self, posts: list) -> list:
# Layer 3: PostgreSQL READ REPLICA for user profile data
# Analytical or non-critical β safe to tolerate replica lag
author_ids = [p['author_id'] for p in posts]
cursor = self.pg_read.cursor()
cursor.execute(
"SELECT id, username, avatar_url FROM users WHERE id = ANY(%s)",
(author_ids,)
)
authors = {row[0]: row for row in cursor.fetchall()}
for post in posts:
post['author'] = authors.get(post['author_id'])
return posts
This example illustrates three different storage layers working together: Redis handles the hot path, Cassandra handles the scalable time-series store, and PostgreSQL (specifically a read replica) enriches the response with structured metadata. Each storage system is doing what it was built to do.
π‘ Mental Model: Think of your databases like the tools on a workbench. A hammer, a screwdriver, and a saw each have a job. Using only one tool for everything is a smell that your design isn't thinking about data access patterns.
Mistake 4: Neglecting Failure Modes β οΈ
A design that only describes the happy path is half a design. Production systems fail β CDN nodes go offline, Redis clusters run out of memory, Kafka consumer lag spikes during traffic bursts, and upstream services return 503s. Senior engineers think about graceful degradation: what does the system do when a dependency fails, and how does it recover?
In social and streaming contexts, failure modes are particularly nuanced because users have different tolerances for different types of degradation.
FAILURE SCENARIO MAP: Social Feed System
ββββββββββββββββ FAILURE ββββββββββββββββββββββββββββββββββββββββββββ
β Component ββββββββββββββββΆβ Graceful Degradation Strategy β
ββββββββββββββββΌββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β Redis Cache β Cache miss β Fall back to DB read replica. β
β β storm β Implement circuit breaker. Shed load β
β β β with stale-while-revalidate headers. β
ββββββββββββββββΌββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β CDN β PoP outage β Geo-DNS failover to secondary PoP. β
β β β Serve lower-resolution media. β
β β β Increase origin TTLs to reduce load. β
ββββββββββββββββΌββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β Write Queue β Kafka lag β Apply backpressure. Disable non- β
β (Kafka) β spike β critical consumers (analytics). β
β β β Alert on consumer lag SLO breach. β
ββββββββββββββββΌββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββ€
β Fanout β Service β Queue writes for retry. Show slightly β
β Service β crash β stale feeds. Do NOT drop writes. β
ββββββββββββββββ΄ββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββ
Two patterns every candidate should name explicitly are the circuit breaker pattern and bulkhead isolation. The circuit breaker stops cascading failures by detecting a failing dependency and short-circuiting calls to it before the failure propagates. Bulkhead isolation prevents one subsystem's failure from exhausting shared resources like thread pools or connection pools.
π€ Did you know? Netflix's engineering team coined the term "chaos engineering" and built Chaos Monkey specifically because they found that teams only truly designed for failure when failures were routine. If you mention resilience testing methodology in an interview, it signals exceptional maturity.
β οΈ Common Mistake: Candidates say "we'd add a retry" without specifying what to retry, how many times, and with what backoff strategy. Unbounded retries during a failure amplify load and turn a partial outage into a total one. Always specify: exponential backoff with jitter and a maximum retry budget.
π‘ Real-World Example: In 2021, a major cloud provider experienced a cascading failure where retry storms caused by a transient DNS blip overloaded their control plane for hours. The root cause wasn't the DNS blip β it was unbounded retry logic in thousands of clients hitting the same endpoint simultaneously.
Mistake 5: Skipping the Estimation Step β οΈ
Perhaps the most persuasive thing you can do in a system design interview is show that your architecture is proportional to the actual load. Skipping back-of-envelope estimation is not a time-saver β it's a red flag that signals you don't have intuition for real-world system constraints.
Estimation doesn't need to be exact. Interviewers know the numbers are approximate. What they're evaluating is whether you can reason about orders of magnitude correctly and use those numbers to justify architectural decisions.
Here is a worked example for a social video platform with 50 million DAUs:
BACK-OF-ENVELOPE ESTIMATION: Video Streaming Platform
ββ USER SCALE ββββββββββββββββββββββββββββββββββββββββββββββ
Daily Active Users (DAU): 50,000,000
Average session length: 30 minutes
Videos watched per session: 6 (avg 5 min each)
ββ READ TRAFFIC ββββββββββββββββββββββββββββββββββββββββββββ
Video views/day: 50M Γ 6 = 300,000,000
Video views/second (peak 3x): 300M / 86,400 Γ 3 β 10,400 QPS
Feed loads/day (3x/session): 50M Γ 3 = 150,000,000
Feed QPS (peak 3x): 150M / 86,400 Γ 3 β 5,200 QPS
ββ WRITE TRAFFIC βββββββββββββββββββββββββββββββββββββββββββ
Uploads/day (1% of users): 500,000 videos
Uploads/second (peak 2x): 500K / 86,400 Γ 2 β 12 UPS
β Read:Write ratio β 867:1
ββ STORAGE βββββββββββββββββββββββββββββββββββββββββββββββββ
Avg compressed video size: 500 MB (1080p, 5 min)
Transcoded versions (Γ4): 500 MB Γ 4 = 2 GB per video
Daily new storage: 500K Γ 2 GB = 1 PB/day β huge!
β Requires tiered storage (hot/warm/cold S3 classes)
ββ BANDWIDTH βββββββββββββββββββββββββββββββββββββββββββββββ
Avg stream bitrate: 5 Mbps (adaptive, avg)
Peak concurrent viewers: 50M Γ 10% = 5,000,000
Required bandwidth: 5M Γ 5 Mbps = 25 Tbps
β Multi-region CDN is non-negotiable
These numbers now drive architectural decisions. The 867:1 read-to-write ratio means you invest heavily in CDN, read replicas, and caching. The 1 PB/day storage growth means S3 with intelligent tiering is mandatory β you can't afford hot storage for 3-year-old videos. The 25 Tbps peak bandwidth confirms that a single origin server is physically impossible β you need a global CDN with 50+ edge locations.
## Encode estimation logic as reusable helper during interview planning
class ScaleEstimator:
"""
Quick estimation calculator to validate architecture decisions.
Walk interviewers through this reasoning process explicitly.
"""
SECONDS_PER_DAY = 86_400
PEAK_MULTIPLIER = 3 # Traffic peaks are ~3x average
@staticmethod
def requests_per_second(daily_requests: int, peak: bool = True) -> float:
"""
Convert daily request volume to peak QPS.
Example:
300M video views/day β ~10,400 peak QPS
This tells us we need 10+ app servers at 1K RPS each.
"""
avg_rps = daily_requests / ScaleEstimator.SECONDS_PER_DAY
return avg_rps * (ScaleEstimator.PEAK_MULTIPLIER if peak else 1)
@staticmethod
def storage_per_year_tb(daily_writes: int, size_mb_each: float) -> float:
"""
Project annual storage growth in terabytes.
Example:
500K uploads/day Γ 2,000 MB each = ~365 PB/year
This forces tiered object storage discussion.
"""
daily_tb = (daily_writes * size_mb_each) / (1024 * 1024) # MB to TB
return daily_tb * 365
@staticmethod
def cache_size_gb(qps: float, avg_object_kb: float, cache_hit_rate: float = 0.9) -> float:
"""
Estimate cache size needed to achieve target hit rate.
A 90% cache hit rate on 10K QPS means only 1K QPS hits the DB.
That's the difference between needing 2 DB replicas vs 20.
"""
unique_qps = qps * (1 - cache_hit_rate)
# Assume working set is ~1 hour of unique requests
working_set = unique_qps * 3600
return (working_set * avg_object_kb) / (1024 * 1024) # KB to GB
## Example usage during interview walkthrough
estimator = ScaleEstimator()
video_peak_qps = estimator.requests_per_second(300_000_000)
print(f"Peak video QPS: {video_peak_qps:,.0f}") # β 10,417
storage_annual = estimator.storage_per_year_tb(500_000, 2_000)
print(f"Annual storage: {storage_annual:,.0f} TB") # β 365,000 TB = ~365 PB
cache_needed = estimator.cache_size_gb(10_417, avg_object_kb=50)
print(f"Cache needed for 90% hit rate: {cache_needed:.1f} GB") # manageable
π― Key Principle: Every major architectural decision should trace back to a number from your estimation. "We need a CDN because our bandwidth requirement is 25 Tbps" is infinitely more convincing than "we need a CDN because it's a best practice."
π§ Mnemonic: Use RUSTLE to remember estimation categories β Requests per second, Users (DAU/MAU), Storage growth, Throughput (bandwidth), Latency targets, Error budget. Cover all six and you've framed your design space completely.
Putting It All Together: The Anti-Pattern Checklist
Before your next design interview, run through this checklist mentally. Each item maps to one of the mistakes above.
π Quick Reference Card: Interview Anti-Pattern Checklist
| # | β Anti-Pattern | β Corrected Pattern | π§ Key Signal to Interviewers |
|---|---|---|---|
| 1 | π« Opened with CQRS/event sourcing | β Started with requirements + simple MVP | Shows judgment, not just knowledge |
| 2 | π« Symmetric read/write architecture | β Separated read path, heavy cache layer | Understands real traffic shapes |
| 3 | π« Single monolithic database | β Polyglot persistence by access pattern | Production-grade data modeling |
| 4 | π« No failure mode discussion | β Named degradation strategy per dependency | Resilience thinking |
| 5 | π« No estimation before architecture | β Numbers first, then architecture | Connects design to real constraints |
The throughline connecting all five mistakes is the same: candidates optimize for appearing knowledgeable rather than demonstrating engineering judgment. The engineers who consistently pass these interviews are the ones who ask clarifying questions, show their reasoning at each step, and treat the interview as a collaborative design session rather than a solo performance.
π‘ Remember: System design interviews are not tests of whether you know the right answer. They are tests of whether you think like an engineer who has shipped production systems. The mistakes above are all, at their core, failures of judgment β and judgment is something you can practice and develop deliberately.
Key Takeaways and Preparing for the Deep-Dive Topics
You've covered a tremendous amount of ground in this lesson. Before you walked in, social and streaming systems might have seemed like magical black boxes β Instagram somehow serves a billion photos a day, Netflix somehow streams to 200 million households simultaneously, and WhatsApp somehow delivers messages in under 100 milliseconds. Now you understand the architectural vocabulary and the engineering decisions that make all of that possible. This final section crystallizes everything you've learned into reusable mental frameworks, connects the dots to the deep-dive lessons ahead, and gives you a concrete self-assessment so you know exactly where you stand.
The Five Architectural Pillars: A Quick-Reference Summary
Every large-scale social or streaming system you'll ever be asked to design in an interview will rely on some combination of the same five architectural pillars. Think of these as the load-bearing walls of any production-grade design. Remove one, and the structure becomes fragile at scale.
π§ Mnemonic: Use the acronym EMCSD β Every Media Company Ships Decisively β to remember Event-driven design, Media pipeline, Caching, Sharding, and DN (CDN).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FIVE PILLARS OF SOCIAL & STREAMING β
βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββ€
β 1. Event- β Decouple producers from consumers. β
β Driven β Kafka/SQS between every major service. β
β β Enables fan-out, audit logs, replay. β
βββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ€
β 2. Media β Ingest β Validate β Transcode β β
β Pipeline β Store β Distribute. Never skip steps. β
β β Async, idempotent, observable. β
βββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ€
β 3. Caching β L1 (local) β L2 (Redis) β L3 (CDN). β
β β Cache hot content, never cold writes. β
β β Invalidation strategy is non-optional. β
βββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ€
β 4. Sharding β Shard by user_id or content_id. β
β β Consistent hashing avoids resharding. β
β β Hot-key mitigation is your safety net. β
βββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ€
β 5. CDN β Push static assets to the edge. β
β β Pull model for long-tail content. β
β β Origin shield prevents cache stampede. β
βββββββββββββββββ΄ββββββββββββββββββββββββββββββββββββββββββ
π Quick Reference Card: The Five Pillars at a Glance
| π§ Pillar | π― Core Problem Solved | β οΈ Failure Mode If Skipped |
|---|---|---|
| π Event-Driven | Tight coupling between services | Cascading failures, no replay |
| π¬ Media Pipeline | Raw content unusable at scale | Broken playback, inconsistent formats |
| β‘ Caching | Database overwhelmed by reads | Read latency explodes, DB crashes |
| ποΈ Sharding | Single node storage/compute limits | Hot spots, write bottlenecks |
| π CDN | Latency for global users | Poor UX in distant regions, high egress cost |
The Decision Framework: Your Mental Checklist
Knowing the pillars is one thing. Knowing when to reach for each one β and being able to justify that choice under interview pressure β is what separates strong candidates from exceptional ones. Before you commit to any architectural component in an interview, run through this five-question mental checklist.
π― Key Principle: In an interview, every architectural decision is a trade-off conversation. You're not being tested on whether you know the right answer β you're being tested on whether you know the right questions.
The Pre-Decision Checklist
π§ 1. What is the read/write ratio? If reads vastly outnumber writes (social feeds, video metadata), lean heavily into caching and CDN. If writes are frequent and reads are proportional (analytics ingestion, logging), lean into event-driven pipelines and sharding strategies that distribute write load.
π 2. How consistent does this data need to be? Is a user's follower count one second stale acceptable? Almost certainly yes. Is a payment transaction one second stale acceptable? Absolutely not. Before adding a cache layer, ask: What's the business cost of stale data here?
π§ 3. What does the access pattern look like over time? Content popularity follows a power-law distribution β a tiny fraction of posts, videos, or accounts receive the overwhelming majority of traffic. Design your hot-key mitigation and tiered caching for that reality, not for the average case.
π― 4. What is the failure blast radius? If this component fails, which users are affected and how severely? A transcoding queue failure shouldn't prevent users from reading their existing feed. Isolate failures by service boundary, and prefer async over sync wherever strict consistency isn't required.
π 5. What will this look like at 10Γ the current scale? The classic interview trap is designing for today. Always state your assumptions, then explicitly ask yourself: does this design bend or break at 10Γ users, 10Γ content, or 10Γ geographic distribution? If it breaks, identify the seam and propose the upgrade path.
π‘ Pro Tip: State your checklist out loud in the interview. Saying "Before I decide on a caching strategy, let me ask about the read/write ratio and the consistency requirements" signals architectural maturity. Interviewers reward the process of reasoning, not just the conclusion.
How This Lesson Maps to the Deep-Dive Topics Ahead
This lesson was deliberately designed as a vocabulary and pattern lesson. Everything you learned here will be applied directly β and in much greater detail β in the specific system design walkthroughs that follow. Here is the explicit mapping so you can approach each child lesson with the right mental scaffolding already in place.
Fan-Out β News Feed Design
In section 2 and 4, you learned about fan-out on write versus fan-out on read and the hot user problem that arises with celebrity accounts. The News Feed design lesson is almost entirely an exercise in answering one question: For a given user at a given moment, how do we cheaply and quickly determine what content belongs in their feed?
You already know the answer in principle: a hybrid fan-out strategy, a Redis-backed timeline cache, and a merge layer at read time for celebrity accounts. The upcoming lesson will walk through the full implementation β database schema, service topology, cache invalidation strategy, and the product edge cases (muted users, deleted posts, privacy changes) that complicate a seemingly simple problem.
Media Pipeline β Video Streaming Design
In section 3, you learned the five-stage media pipeline: ingest, validate, transcode, store, and distribute. The Video Streaming design lesson takes that pipeline and asks: What does each of these stages look like in practice, at Netflix or YouTube scale?
You'll go deep on adaptive bitrate streaming (ABR), how HLS and MPEG-DASH manifests work, how segment servers coordinate with CDN edge nodes, and what the origin shield architecture looks like when you're serving 100 million concurrent streams. The conceptual foundation you built here β particularly around CDN pull vs. push and tiered storage β will make those details click immediately.
Real-Time Messaging β Chat System Design
In section 2, you were introduced to WebSockets and long polling as the two primary mechanisms for pushing real-time messages to clients. The Chat System design lesson builds directly on that, asking: How do you maintain persistent, low-latency bidirectional connections across millions of concurrent users, guarantee message delivery, and handle offline users?
Your event-driven architecture knowledge will be essential here β message queues between the gateway layer and the storage layer are what enable guaranteed delivery semantics. Your sharding knowledge applies directly to how conversation data is partitioned. Nothing in the Chat lesson will feel foreign.
LESSON MAP: How Foundations Flow into Deep Dives
βββββββββββββββββββββββββββββββββββββββββββββββ
β THIS LESSON (Foundations) β
β β
β Event-Driven βββββββββββββββββββββββββββ β
β Media Pipeline ββββββββββββββββββββββ β β
β Caching + Sharding βββββββββββββββ β β β
βββββββββββββββββββββββββββββββββββββΌβββΌβββΌβββ
β β β
βββββββββββββββββ β βββββββββββββββββ
βΌ βΌ βΌ
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
β News Feed β β Streaming β β Chat β
β (fan-out, β β (pipeline, β β (WS, msg β
β timeline β β ABR, CDN) β β delivery, β
β cache) β β β β presence) β
ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ
A Practical Design Habit: Sketch Apps You Use Daily
The single most effective low-effort practice habit for system design is also the simplest: every time you open a social or streaming app, take 90 seconds to mentally sketch its high-level architecture.
You open Twitter/X. Ask yourself: How does my feed get populated? Probably fan-out on write for most accounts, fan-out on read for celebrities. Where are the tweets stored? Likely a sharded relational store plus a cache layer. How are images served? CDN, definitely. How do notifications reach me in real time? WebSocket connection to a notification gateway.
You open Spotify. Ask yourself: How does the song start playing in under 500ms? Pre-loaded buffer from CDN edge, adaptive bitrate for network conditions. How does it know my recently played? A Redis cache keyed on my user ID. How do playlist changes sync across my devices? Event-driven, probably a change-data-capture stream from the playlist service.
This habit does three things for you. First, it forces you to apply abstract patterns to concrete products, which cements the concepts far better than re-reading notes. Second, it trains you to spot the questions you can't answer yet β which are exactly the gaps worth filling before your interview. Third, it builds a library of real-world examples you can reference naturally in an interview when an interviewer asks, "Have you seen this pattern used in practice?"
π‘ Real-World Example: A senior engineer preparing for FAANG interviews described this habit as "architectural tourism." Every app became a museum exhibit. After three weeks, she reported that interview questions felt like being asked to describe a place she'd already visited rather than imagine somewhere she'd never been.
Here's a practical code snippet to illustrate this thinking habit in action. Imagine you're sketching the back-end of a notification service β something you'd encounter in News Feed, Chat, and Streaming alike:
## High-level sketch of a notification dispatch service
## This mirrors the kind of pseudocode you'd walk through in an interview
import asyncio
from dataclasses import dataclass
from enum import Enum
class DeliveryChannel(Enum):
WEBSOCKET = "websocket" # User is online: push immediately
PUSH_NOTIFICATION = "push" # User is offline: mobile push
EMAIL = "email" # Digest for low-priority events
@dataclass
class NotificationEvent:
user_id: str
event_type: str # e.g., "new_follower", "post_liked"
payload: dict
priority: int # 1 = high (chat msg), 3 = low (weekly digest)
class NotificationDispatcher:
def __init__(self, presence_service, ws_gateway, push_gateway):
self.presence = presence_service # Redis-backed: is user online?
self.ws = ws_gateway # WebSocket connection pool
self.push = push_gateway # APNs / FCM client
async def dispatch(self, event: NotificationEvent):
"""
Route notification based on user presence and event priority.
This is the fan-out decision point β one event, one user.
At scale, this runs in parallel for each recipient.
"""
is_online = await self.presence.check(event.user_id)
if is_online:
# User has an active WebSocket β deliver in real time
await self.ws.send(event.user_id, event.payload)
elif event.priority <= 1:
# High-priority, user offline β use mobile push
await self.push.send(event.user_id, event.payload)
else:
# Low-priority, user offline β batch for email digest
await self.queue_for_digest(event)
async def queue_for_digest(self, event: NotificationEvent):
# Enqueue to Kafka topic consumed by email digest service
# Demonstrates event-driven decoupling: this service
# doesn't care how or when the digest is sent
await kafka_producer.send("email-digest-queue", event)
This snippet demonstrates several pillars simultaneously: event-driven dispatch (Kafka for low-priority events), presence-aware routing (Redis cache for online status), and clear service separation (this dispatcher knows nothing about email rendering). In an interview, walking through code like this shows you can translate architecture diagrams into concrete implementation thinking.
Self-Assessment Checklist: Are You Ready for the Deep Dives?
Before moving on to the specific system design walkthroughs, you should be able to answer the following five questions confidently and without notes. These aren't trick questions β they're a direct test of whether the foundational concepts have landed.
Take a moment with each one. If you find yourself uncertain, note the section of this lesson to revisit before proceeding.
Question 1: Explain fan-out on write vs. fan-out on read, and describe when you'd choose each.
β Confident answer looks like: Fan-out on write pre-populates each follower's timeline cache at post time, giving fast reads but expensive writes for high-follower accounts. Fan-out on read assembles the timeline at request time, scaling writes cheaply but adding read latency. Hybrid: fan-out on write for regular users, fan-out on read (with merge) for celebrities.
Question 2: Walk through the five stages of a video media pipeline.
β Confident answer looks like: Ingest (raw upload to object storage) β Validate (format, size, policy checks) β Transcode (generate multiple bitrate/resolution variants) β Store (distributed object storage with metadata in DB) β Distribute (push hot content to CDN edge, serve via signed URLs).
Question 3: What is consistent hashing, and why does it matter for sharding?
β Confident answer looks like: Consistent hashing maps both data keys and nodes onto a ring, so adding or removing a node only remaps a fraction of keys (1/N on average), avoiding a full resharding. It matters because social platforms add nodes frequently as they scale, and a full reshuffle would cause massive cache invalidation and database load.
Question 4: Describe three distinct caching layers in a social platform and what each caches.
β Confident answer looks like: L1 in-process cache (hot feed objects for the current request), L2 distributed cache like Redis (user timelines, session data, follower counts), L3 CDN edge (static assets, thumbnails, video segments). Each layer trades capacity for latency β L1 is fastest but smallest, CDN is largest but for read-only static content.
Question 5: What does an origin shield do in a CDN architecture?
β Confident answer looks like: An origin shield is a dedicated mid-tier CDN cache node sitting between edge nodes and the origin server. When many edge nodes simultaneously miss on the same content (cache stampede), they all hit the shield rather than the origin, collapsing N requests into one. It dramatically reduces origin load and prevents cascading failures during high-traffic events.
Here's a small code snippet that illustrates the origin shield concept in pseudo-implementation β the kind of clarifying code you might sketch on a virtual whiteboard:
// Simplified CDN request routing logic
// Demonstrates origin shield as a cache collapse layer
async function handleCdnRequest(contentId, edgeNodeId) {
// Step 1: Check edge node's local cache first (L3 edge)
const edgeCached = await edgeCache.get(contentId);
if (edgeCached) {
return { source: 'edge', data: edgeCached };
}
// Step 2: Miss at edge β go to origin shield, NOT origin directly
// This is the key: all edge nodes funnel through one shield node
// preventing N simultaneous origin requests for the same content
const shieldCached = await originShield.get(contentId);
if (shieldCached) {
// Backfill the edge cache for future requests from this region
await edgeCache.set(contentId, shieldCached, TTL_SECONDS);
return { source: 'shield', data: shieldCached };
}
// Step 3: Total miss β shield fetches from origin (one request, not N)
// A distributed lock ensures only one fetch happens concurrently
const originData = await originShield.fetchFromOrigin(contentId);
await edgeCache.set(contentId, originData, TTL_SECONDS);
return { source: 'origin', data: originData };
}
This code shows the three-tier lookup pattern explicitly. In the interview, drawing this as a flowchart or writing pseudocode like this is far more impressive than simply saying "CDN reduces latency."
Final Critical Points to Carry Forward
β οΈ The five pillars are a floor, not a ceiling. Every real system you'll be asked to design uses all five pillars simultaneously and in combination. The skill is knowing how they interact β a CDN without a sharding strategy for the origin can still become a bottleneck; caching without an invalidation strategy breeds consistency bugs. Think in systems, not in isolated components.
β οΈ Justify every decision with a trade-off. An interviewer who hears "I'd use Redis for caching" will always follow up with "Why Redis? Why not Memcached? Why not a local cache?" The answer to every architectural choice lives in the trade-offs: Redis supports richer data structures and persistence; Memcached is simpler and faster for pure key-value lookups; local cache eliminates network hops but doesn't share state across instances. Know your trade-offs before you name your technology.
β οΈ Scale numbers ground your design. Before you commit to any architecture, anchor it to numbers. "We're designing for 100 million daily active users, each generating 10 events per day. That's 1 billion events per day, roughly 11,500 events per second at peak. A single Kafka partition handles ~50,000 messages per second β we need at minimum 1 partition, but we'd design for 20 for headroom." Numbers like these transform vague architecture into credible engineering.
π‘ Remember: The interviewers who designed these systems at scale are your audience. They don't want to hear that you've memorized patterns β they want to see that you think the way those patterns emerged. Ask the right questions, reason from first principles, and let the architecture follow from the constraints.
Your Next Steps
π― Step 1: Complete the self-assessment. If any of the five questions above felt uncertain, open the relevant section of this lesson and re-read it with the specific question in mind. Targeted re-reading is far more effective than passive review.
π Step 2: Do one daily sketch exercise. Pick any social or streaming app you used today and sketch its architecture β even if it's just a napkin diagram. Five minutes of active recall is worth an hour of re-reading.
π§ Step 3: Move to the News Feed deep-dive first. Of the three upcoming lessons, News Feed is the most foundational because it is almost entirely an exercise in the fan-out and caching patterns you learned here. Nail that one, and the Streaming and Chat lessons will feel like variations on a theme you already know well.
You've built the architectural vocabulary. You have the mental checklist. You understand the trade-offs. The deep dives ahead are where these foundations get tested against the full complexity of real-world systems β and you're ready for them.