Photo/video feed, uploads, stories, direct-to-S3 media, hybrid fan-out feed, ML ranking.
The scenario
Photo sharing at 2B users — where the ranking model is the product
Same startup. Same engineer #4. Different Monday from the URL Shortener and Slack you've already shipped.
Your CTO drops by with coffee — and she's grinning this time. *"Users love the platform. They want a photo-sharing feature. Photos and short videos. Comment. Like. Follow. A feed that surfaces what they'd actually want to see. Ship an MVP in 12 weeks. Same design discipline as last time. But this one has a twist you haven't hit yet — the ranking is the product."*
She walks away.
Here's the new pattern to internalize before we design a single component:
URL Shortener was read-heavy. 100 redirects per URL created. Your job was to make ~40µs cache hits and avoid DB stampedes.
Slack was real-time write-heavy. Every message fans out to N members in <100ms. Your job was to route WebSockets by team affinity so writes and pushes don't cross-talk.
Instagram is a ranking system that also happens to store photos. The storage is trivial: S3 + a CDN. But at 2B MAU (Meta Q4 2024 earnings), the feed — deciding which of ~10,000 candidate posts to show you when you open the app — is a deep-learning ranking system running on custom silicon (Meta DLRM paper).
In 2022 Instagram made a fundamental shift: more than 50% of feed impressions are now from accounts you don't follow, ranked by an ML model (Adam Mosseri, 2022). That means the "who to follow" table you'd design at L4 is the wrong data model at L7. Understanding when the model breaks is the whole game.
Here's the shape you're going to design across the next 12 chapters:
The whole journey at a glance
Every 10× in user scale forces a fundamentally different feed generation strategy — and the crossover from "pull-based JOIN" to "pushed-fanout" to "ML-ranked candidate set" is the story:
text═══════════ INSTAGRAM ARCHITECTURE ACROSS 4 SCALES ═══════════ L4 (10K users) L5 (100M users) L6 (1B users) L7 (2.4B users) boring stack fanout + shard global regions ML ranks EVERYTHING 12 weeks · $500/mo 6 months · $50K/mo 18 months · $500K/mo ongoing · $5B/yr infra ┌────────┐ ┌────────┐ ┌──── Global CDN ────┐ ┌── Custom silicon ──┐ │ Web │ │ Web │ │ photos + videos │ │ Meta MTIA + GPU │ │ Mobile │ │ Mobile │ │ edge caches │ │ DLRM inference │ └───┬────┘ └───┬────┘ └─┬──┬──┬─┬──────────┘ └──┬──┬──┬──┬──────┘ │ │ │ │ │ │ │ │ │ │ ┌──▼───┐ ┌──▼──┐ ┌──▼──▼──▼──▼─────┐ ┌──▼──▼──▼──▼──────┐ │ ALB │ │ ALB │ │ Multi-region │ │ 20+ regions + │ │ │ │ │ │ ALB · anycast │ │ edge presence │ └──┬───┘ └──┬──┘ └───┬──────┬──────┘ └────┬──────┬──────┘ │ │ │ │ │ │ ┌──▼──────┐ ┌───▼─────────┐ ┌───▼──────▼───┐ ┌───────▼──────▼──────┐ │ 1 API │ │ API pods │ │ Regional │ │ 50+ ML services │ │ server │ │ sharded │ │ API + fanout│ │ DLRM ranker │ │ │ │ by user_id │ │ service │ │ candidate gen │ └──┬──────┘ │ │ │ │ │ seed exposure │ │ └───┬─────────┘ └───┬──────────┘ │ causal recommend │ │ │ │ └──┬──┬──┬──┬─────────┘ │ ┌───▼──┐ ┌───▼──────────┐ │ │ │ │ │ │Redis │ │Redis Cluster │ ┌──▼──▼──▼──▼────┐ │ │feed │ │per-region │ │TAO graph store │ │ │cache │ │feed cache │ │(Meta's cache) │ │ └───┬──┘ └───┬──────────┘ │+MyRocks tier │ │ │ │ └──┬─────────────┘ ┌──▼───┐ ┌───▼─┐ ┌──▼──────────┐ │ │MySQL │ │MySQL│ │Sharded MySQL│ ┌──▼──────────────┐ │+ S3 │ │+ S3 │ │+ Vitess/TAO │ │Cassandra + │ │metadata │+ CDN│ │+ Cassandra │ │HDFS + Manifold │ │photo binaries │ │ │per-region │ │object store │ └──┬───┘ └──┬──┘ └───┬─────────┘ │(Meta's S3) │ │ │ │ └─────────────────┘ ┌──▼───┐ ┌──▼──┐ ┌───▼──────────┐ │ S3 │ │ S3 │ │ Kafka + │ │binary│ │+CDN │ │ Flink for │ │store │ │worker│ │ transcoding │ └──────┘ │pool │ │ + ML sig │ └─────┘ └──────────────┘ ↑ ↑ ↑ ↑ Boring Chronological Push-based fanout ML predicts what you want. pull-based feed + follows. for followers + 50%+ from accounts you JOIN works. Fanout starts ML-ranked "explore". don't follow. DLRM on hurting at 100M. Regional isolation. custom silicon (MTIA). Chapter 5 Chapters 6+6.5 Chapter 7+7.5 Chapter 8 walks walks through walks through walks through through the fanout the DLRM ranker, Meta's custom hardware, L4 pull vs. pull-merge TAO graph store, the "explore" shift, and feed in full trade-off and multi-region cost as strategy Cassandra tier Key insight: at Instagram scale, storage is trivial. The PRODUCT is the model that ranks 10,000 candidate posts down to the 20 you'll see. That's the whole game — and that's why 2022's "50% from accounts you don't follow" shift was a fundamental data-model change, not just a UX tweak.
The same 4 tiers as clean architecture diagrams
The ASCII compressed all four tiers side-by-side. Here are the same four scales as clean Mermaid flowcharts — the shapes an interviewer expects on a whiteboard:
L4 · 10K users · pull-based JOIN · $500/mo · 12 weeks:
flowchart TD
W([Web / Mobile clients]) -->|HTTPS| LB[ALB<br/>$25/mo]
LB --> API[1 API server<br/>c5.large<br/>~5K RPS]
API --> PG[(MySQL Multi-AZ<br/>photos + users + follows<br/>$150/mo)]
API -->|presigned PUT| S3[(S3<br/>photo binaries<br/>~$50/mo)]
S3 --> CF[CloudFront CDN<br/>photo viewer path<br/>$100/mo]
W -.->|GET photo| CF
classDef n fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
class LB,API,PG,S3,CF nL5 · 100M users · fanout + hybrid · $50K/mo · 6 months:
flowchart TD
W([Web / Mobile]) -->|HTTPS| LB[ALB · WebSocket for stories<br/>WSS]
LB --> R{User-affinity<br/>router<br/>hash by user_id}
R --> A1[API 1]
R --> A2[API 2]
R --> A3[API 3]
A1 --> RD[(Redis feed cache<br/>hot posts · followers)]
A2 --> RD
A3 --> RD
A1 --> KF[Kafka fanout<br/>new-post events]
KF --> FW[Fanout workers<br/>write to N follower feeds]
FW --> RD
A1 --> PG[(MySQL primary<br/>+ 2 read replicas<br/>photos · users · follows)]
A2 --> PG
A3 --> PG
A1 --> S3[(S3 + CloudFront<br/>photos · videos ABR)]
classDef n fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef c fill:#fef3c7,stroke:#d97706,color:#78350f
class LB,R,A1,A2,A3 n
class KF,FW,RD,PG,S3 cL6 · 1B users · global regions + DLRM ranking · $500K/mo · 18 months:
flowchart TD
W([Global users]) -->|HTTPS anycast| CDN[Global CDN<br/>Meta edge]
CDN --> LB[Regional ALB<br/>us-east · eu-west · ap-northeast]
LB --> API[Regional API pods<br/>50 pods per region]
API --> RANK[DLRM candidate<br/>generation service<br/>~10K candidates → 20]
RANK --> TAO[(TAO graph store<br/>Meta's cache tier<br/>social graph reads)]
RANK --> MRK[(MyRocks<br/>counter store)]
API --> RD[(Redis Cluster<br/>per-region feed cache)]
API --> C[(Cassandra per-region<br/>photos · counters · analytics)]
API --> KF[Kafka + Flink<br/>fanout · transcoding<br/>ML signals]
classDef n fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef m fill:#fce7f3,stroke:#be185d,color:#831843
class CDN,LB,API,RD n
class RANK,TAO,MRK,C,KF mL7 · 2.4B users · custom silicon + explore shift · $5B/yr infra:
flowchart TD
W([Global users]) -->|HTTPS anycast| CDN[Meta's Own CDN<br/>edge caches + video]
CDN --> LB[20+ regional ALBs]
LB --> API[Regional API]
API --> RANK[DLRM ranker<br/>on Meta MTIA silicon<br/>+ GPU cluster]
RANK --> CG[Candidate generation<br/>50%+ from non-follow]
RANK --> SE[Seed exposure<br/>onboarding ranker]
RANK --> CR[Causal recommender<br/>counterfactual]
CG --> TAO[(TAO + memcache<br/>Meta social graph)]
CG --> MF[(Manifold<br/>Meta's object store)]
CG --> HDFS[(HDFS<br/>ML training data)]
API --> C[(Cassandra multi-region<br/>replicated 3x)]
classDef n fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef m fill:#fce7f3,stroke:#be185d,color:#831843
classDef ml fill:#dcfce7,stroke:#16a34a,color:#14532d
class CDN,LB,API n
class C,TAO,MF,HDFS m
class RANK,CG,SE,CR mlWhy every 10× breaks the architecture
Look at the four tiers side by side. Notice how the storage tier barely changes — MySQL → Sharded MySQL → Cassandra. That's boring engineering, and it's fine.
What DOES change dramatically:
- Feed generation. L4 = JOIN across photos + follows. L5 = pushed fanout to follower Redis lists. L6 = candidate generation + ranking. L7 = ML ranking is the entire product.
- The role of the "who I follow" graph. L4-L5 = the graph IS the feed source. L6-L7 = the graph is one signal among 50 the ranker uses.
- The compute:cpu ratio flips. L4 = 90% storage/network, 10% CPU. L7 = 60% ML inference on custom silicon, 40% everything else.
The 3 senior insights before we start Chapter 1
- Storage tiering is table stakes; ranking is the product. At L4 you'll want to overinvest in the DB schema. Resist. Photos → S3 + CDN. Metadata → MySQL. Move on. The interesting problem is the feed ranker.
- Fanout has a cliff. Push fanout (write photo → write to N follower feeds) works up to ~10K followers per user. Above that (celebrities, verified accounts), you MUST pull-merge at read time. Instagram uses hybrid — push for regular users, pull for celebs. That decision comes in Chapter 6.
- The 2022 "explore" shift changed the interview answer. If you say "the feed shows posts from accounts I follow, ranked by time", you're describing Instagram 2015. Since 2022, Instagram's own product blog announced that more than half of impressions are from accounts you don't follow — chosen by a deep-learning ranker over a pool of ~10,000 candidates. If you don't mention this shift, the interviewer will probe.
Chapter map for the journey ahead
- Chapter 1 — Requirements (functional + non-functional, per level)
- Chapter 2 — Capacity estimation (2B MAU, 100M photos/day upload, 1T impressions/day feed reads)
- Chapter 3 — API design (upload, feed, engagement, follow)
- Chapter 4 — Data model (photos, follows, likes — and why NOT Postgres for social graph)
- Chapter 4.5 — The follows graph: adjacency list vs graph store vs materialized fanout
- Chapter 5 — L4 MVP: pull-based feed with JOIN. Works up to ~10K users
- Chapter 6 — L5: fanout with Redis feed caches. The cliff at 10K followers
- Chapter 6.5 — Hybrid push/pull for celebrities. TAO-inspired graph store
- Chapter 7 — L6: DLRM ranker. Candidate generation. Regional Cassandra
- Chapter 7.5 — The ranking pipeline: signals, training, serving on custom silicon
- Chapter 8 — L7: the "explore" shift. Multi-region ML. Cost as strategy
- Chapter 9 — Failure modes: ranker outage, follow-graph inconsistency, viral post melting the cache
- Chapter 10 — Trade-off matrix
- Chapter 11 — Interview masterclass: 45-min mock, questions to ask
- Chapter 12 — Defense: the 20 hardest interview questions on Instagram
Ready? Chapter 1 next: understanding what the CTO actually asked for.
Instagram at 2.4B MAU is a ranking system that also happens to store photos. Storage is trivial (S3 + CDN); the product is the DLRM ranker choosing 20 of ~10,000 candidates. The 2022 shift where 50%+ of impressions come from non-followed accounts is a fundamental data-model change from every pre-2022 tutorial.
- Why is Instagram fundamentally a ranking system, not a storage system?
- What does 2B MAU imply for the architecture (regional isolation, custom silicon)?
- What was the 2022 'explore' shift and why does it change the interview answer?
- When does push-fanout hit a cliff, and what replaces it?
- What's the difference between L4 (JOIN) and L7 (DLRM inference) feed generation?
Every concept below has its own interactive, animated page in the Learning Tracks section. Read them any time you want to go deeper than the mentor prose above — they're the reusable foundation this chapter is built on.
The algorithm behind user-affinity routing at L5 and shard placement in L6-L7 Cassandra + TAO.
TAO's read-through cache invalidation is what makes the follows graph fast enough to power the DLRM ranker at 2B MAU.
Chapter 1 next: what did the CTO actually ask for? Photo upload, feed, engagement, follow — each has functional and non-functional requirements. Get these wrong and you'll design the wrong system for the whole 12 chapters.
Components used in this design
Study each component's deep dive to understand it in isolation.
Load Balancer
Distributes incoming traffic across a pool of servers for scale and fault tolerance.
CDN (Content Delivery Network)
A globally distributed cache that serves static and cacheable dynamic content close to the user.
Redis
An in-memory key-value store used for caching, pub/sub, rate limiting, distributed locks, and simple queues.
SQL Database
A row-oriented, ACID-compliant relational database — the default for transactional workloads.
NoSQL Database
Umbrella for document, wide-column, and key-value stores optimized for horizontal scale over strict schema.
Kafka
A distributed, partitioned, replicated commit log for event streaming, high-throughput ingest, and decoupled services.
API Gateway
Single entry point that handles auth, rate limiting, routing, and protocol translation for downstream services.
Object Storage (S3, GCS, Azure Blob)
Durable, cheap, flat-namespace storage for blobs — images, videos, backups, logs, and any large binary object.
Search Engine (Elasticsearch, OpenSearch, Meilisearch)
Inverted-index-based full-text and structured search — the right tool for 'find me records matching this query' when SQL LIKE isn't fast enough.
Rate Limiter
Enforces per-caller (per-user, per-IP, per-tenant) request budgets to protect downstream systems from abuse and overload.
Stream Processor (Flink, Kafka Streams, Spark Structured Streaming)
Continuous computation over event streams — windowed aggregations, joins, ML features, and event-driven derived views.
Message Queue
Async task queue that decouples producers from consumers and smooths bursts. Each message is processed exactly once (per-message ACK model).
Service Discovery (Consul, etcd, DNS-based)
How services find each other in a dynamic fleet — because IPs change, nodes come and go, and hardcoding is a losing game.
Patterns applied in this design
Study each pattern's deep dive for the recurring solution logic.
Cache-aside (Lazy loading)
Reads dominate your workload, and every read hits a slow store (database, disk, network). Latency and load on the primary storage climb until the primary becomes the bottleneck.
Sharding (Horizontal partitioning)
Your dataset or write throughput has outgrown a single node. A single primary DB or single cache node can't hold the data or handle the QPS.
Consistent hashing
You need to shard keys across N nodes, but N changes over time (nodes added, removed, or failed). Naive hash-mod-N reshuffles almost everything on every change — cache is wiped, migration cost is huge.
Rate limiting
One bad actor can consume all your capacity. And even good actors need bounds so you can capacity-plan.
Explore next — related systems
If you enjoyed this problem, these share similar patterns or challenges.