Source: https://www.hack2hire.com/question-bank/questions/system-design/69e1b8b295e80ae213e5d4a2
Company: Meta, Microsoft, Google
Difficulty: Hard
Problem statement
Users post photos and videos, follow each other and scroll a feed; design upload, storage, feed generation and serving media worldwide.
Requirements
We are building a photo and video sharing service: users upload posts, follow other accounts, and scroll a home feed of posts from people they follow, with media served fast anywhere in the world.
Functional requirements
- Upload a post: 1–10 photos or a short video (≤ 60 s), a caption, optional location and hashtags.
- Follow and unfollow accounts; private accounts require an approved request.
- Home feed: posts from followed accounts, newest first (ranking comes later as a separate layer), paginated with infinite scroll.
- Profile page: a user's posts in a grid, with follower and following counts.
- Like and comment on posts (counts shown on the feed; full comment threads are a supporting service).
- Out of scope for this answer: Stories, Reels recommendations, DMs, ads, search beyond a short note on hashtags.
Non-functional requirements
- Feed load p99 under 300 ms for the first page (metadata); first image visible under 1 s on a 4G connection.
- Upload is reliable: once the client gets an ack, the post is never lost, even if processing fails and retries.
- A new post shows in followers' feeds within a few seconds (eventual consistency is fine); the author sees it immediately (read-your-writes).
- Read-heavy: around 100 feed reads per post written. Availability over consistency for feeds and counts.
- Media durable at 11 nines; served from a CDN in every region.
Back-of-the-envelope estimation
- 500M daily active users, 100M new posts/day ≈ 1,200 posts/s average, ~5,000/s peak.
- Feed reads: each DAU opens the feed ~10 times → 5B feed pages/day ≈ 60k/s average, ~200k/s peak.
- Media: an average photo post is 2 MB original; we keep ~4 resized variants totaling ~1 MB more. 100M × 3 MB ≈ 300 TB/day of new storage, ~110 PB/year before replication. Videos dominate bytes; tier old media to cheaper storage.
- Egress: each feed page shows ~10 images at ~150 KB → 5B × 1.5 MB ≈ 7.5 PB/day, almost all from CDN. Origin must see well under 5% of that.
- Follow graph: average 200 followees; 500M × 200 × 16 B ≈ 1.6 TB per direction, so store both directions.
- Precomputed timelines: keep the latest 500 post IDs per active user at 16 B each ≈ 8 KB/user → 500M × 8 KB ≈ 4 TB in a Redis cluster.
API design
Clients send an Idempotency-Key header on every write. Media bytes never go through the API servers; clients upload straight to object storage with signed URLs.
POST /v1/media/uploads
{count: 3, type: "photo", sizes: [2100000, 1800000, 2400000]}
→ {upload_id, parts: [{media_id, put_url, expires_at}]} # presigned, multipart for video
POST /v1/posts
{upload_id, media_ids: [...], caption, location_id?, client_ts}
→ 202 {post_id, status: "processing"}
GET /v1/posts/{post_id}
→ {post_id, author, caption, media: [{variants: {w320, w640, w1080}, blurhash, aspect}], like_count, created_at, status}
GET /v1/feed?cursor=<opaque>&limit=20
→ {items: [{post...}], next_cursor, new_items_hint: 4}
POST /v1/users/{id}/follow → {state: "following" | "requested"}
DELETE /v1/users/{id}/follow → 204
GET /v1/users/{id}/posts?cursor=... # profile grid
POST /v1/posts/{id}/likes → 204 (DELETE to unlike)
The cursor is opaque base64 of {last_score, last_post_id, snapshot_id}, never an offset.
Data model
| Store | Key / table | Contents | Why this store |
|---|---|---|---|
Sharded SQL (MySQL/Postgres, shard by user_id) |
users(user_id PK, handle UNIQUE, is_private, follower_count, ...) |
Profiles | Strong consistency, unique handles |
Same, shard by author_id
|
posts(post_id PK, author_id, caption, status, media_ids JSON, created_at), index (author_id, created_at DESC)
|
Post metadata | Profile grid is one shard range scan |
| Wide-column (Cassandra) |
followers(followee_id PK, follower_id CK) and following(follower_id PK, followee_id CK)
|
Both edge directions | Huge fan-in lists, cheap appends, partition per user |
| Wide-column | media(media_id PK, owner_id, state, variants map, checksum) |
Processing state | High write rate, simple key lookups |
| Redis cluster |
tl:{user_id} sorted set: member post_id, score created_at ms, capped at 500 |
Precomputed home timeline | Sub-ms reads, trimmed on write |
| Redis / counters service | cnt:post:{id}:likes |
Like counts, flushed to SQL in batches | Absorbs hot-post write bursts |
| Object storage (S3/GCS) | media/{shard}/{media_id}/{variant}.jpg |
Originals and variants | Cheap, durable, CDN origin |
Post IDs are 64-bit Snowflake-style (ms timestamp + shard + sequence), so they sort by time and carry the author shard. Celebrity flag lives on users (follower_count > 1M).
High-level design
flowchart LR
C[Mobile client] -->|presigned PUT| S3[(Object storage)]
C --> GW[API gateway]
GW --> PS[Post service]
GW --> FS[Feed service]
GW --> GS[Graph service]
PS --> PDB[(Posts SQL)]
PS --> Q[[Kafka]]
S3 -->|upload event| MP[Media pipeline]
MP --> S3
MP --> Q
Q --> FO[Fanout workers]
FO --> GDB[(Follow graph)]
FO --> TL[(Timeline Redis)]
FS --> TL
FS --> PC[(Post cache)]
GS --> GDB
GS --> Q
C -->|images| CDN[CDN edge]
CDN --> S3
-
Upload. The client asks for presigned URLs, uploads originals directly to object storage, then calls
POST /posts. The post row is written withstatus = processingand the author sees it at once from their own profile. -
Process. The storage upload event starts the media pipeline: validate, strip EXIF, make variants, compute a blurhash, run safety checks. When every media item is ready, it flips the post to
publishedand emitsPostPublishedto Kafka. -
Fanout. Fanout workers read the author's follower list in pages of 1,000 and
ZADDthe post ID into each active follower'stl:set, then trim it to 500. Celebrity authors are skipped (see hybrid fanout). -
Read feed. The feed service reads
tl:{viewer}, merges in recent posts from followed celebrities, drops deleted or blocked items, then hydrates post IDs in a batch from the post cache. It returns metadata plus CDN URLs. -
Follow/unfollow. The graph service writes both edge tables and emits
FollowChanged; a worker backfills or removes that author's recent posts in the viewer's timeline. - Media delivery. The client chooses a variant by screen width and fetches it from the nearest CDN edge. Misses go to a regional shield cache, then to origin storage.
Deep dives
Media processing pipeline: from upload to renderable.
- Uploads are resumable (multipart, 5 MB chunks) so a dropped connection on mobile resumes instead of restarting. The client compresses to ≤ 1080 px wide before upload, which cuts bytes about 3×.
- The upload event enqueues a job keyed by
media_id, which makes it idempotent: a retry overwrites the same variant keys. Steps are validate → decode → strip EXIF and fix orientation → resize to 150 (thumbnail), 320, 640, 1080 → encode WebP/AVIF plus a JPEG fallback → blurhash → safety classifier. - Video is transcoded to HLS at 3–4 bitrates with 2–4 s segments on a GPU pool. That takes seconds to minutes, so the post stays
processingand the client shows a local preview meanwhile. - A post publishes only when all its media are
ready. A sweeper retries media that stay stuck past 10 minutes and marks themfailedafter 3 attempts, then notifies the author. Originals stay for re-encoding when new formats ship. - Photo CPU: ~200 ms per photo across all variants → 5,000 posts/s × 2 photos × 0.2 s ≈ 2,000 cores at peak. Autoscale on queue depth.
Hybrid fanout: cheap reads without celebrity write explosions.
| Strategy | Write cost per post | Read cost | Problem |
|---|---|---|---|
| Fanout on write (push) | one ZADD per follower | one Redis read | 300M followers → 300M writes per post |
| Fanout on read (pull) | 1 row | query N followees, merge | 200 followees × 200k reads/s is far too slow |
| Hybrid | push for normal authors | timeline + pull from ≤ ~50 celebrities | Needs a merge step on read |
- Push for authors with < 1M followers; pull for authors above that. Average fanout is ~200, so 5,000 posts/s → ~1M ZADDs/s across the Redis cluster. That is fine when spread over ~100 shards.
- Push only to followers active in the last 30 days. For an inactive user, the timeline is rebuilt with a pull when they return, which cuts the write volume a lot.
- On read, fetch each followed celebrity's recent posts from a per-author cache (
author:{id}:recent, the last 50 IDs). These are very hot and cache well. Merge by score with the timeline set. - The threshold has hysteresis (1M to switch on, 800k to switch off), so authors near the line don't flip back and forth.
Cursor pagination when the feed keeps changing.
- Offset pagination breaks: new posts at the top shift every item down, so the next page repeats items. Deletes and fanout from late-arriving posts shift items too.
- Use a keyset cursor
(score, post_id): page 2 isZREVRANGEBYSCORE tl:{u} (last_score -inf LIMIT 0 20, withpost_idbreaking ties. Inserts at the top never affect later pages. - Ranked feeds have scores that change between requests, so on the first page we freeze the ranked ID list for 15 minutes under
snapshot_id(~500 IDs, 8 KB). Later pages read that snapshot. New posts appear as a "new posts" pill (thenew_items_hint), not mixed in. - The client dedupes by
post_idto cover the rare edge case. Deleted posts are filtered when hydrating, so a page can return 19 items. That is acceptable; we never backfill inside a page.
CDN delivery, cache warming and invalidation.
- Media URLs are immutable and content-addressed (
/{media_id}/{variant}-{hash}.webp), so we cache them for a year and never need invalidation for edits. A delete removes the object at origin and issues a purge, and also revokes access through signed URL expiry for private accounts. - Tiered caching (edge → regional shield → origin) keeps origin egress near 1–2%. Popular new posts from big accounts get pre-pushed to shields in the regions where most of their followers live.
- Private-account media use short-lived signed URLs (e.g. 1 h) generated at feed hydration time. The CDN validates the signature, so cached objects never leak to people who don't follow the account.
- Lifecycle: variants older than 90 days with low access move to infrequent-access storage. Rarely requested variants are dropped and regenerated on demand from the original.
Follow changes, backfill and repair.
- Follow: insert both edges, then a worker pulls the followee's last ~20 posts and
ZADDs them to the viewer's timeline, so the feed changes right away. Unfollow: remove that author's IDs with a scan of the 500-item set. That is cheap at this size. - Race: a post being fanned out at the same moment as an unfollow can land after the removal. The feed service filters hydrated items against the viewer's current following set (a cached bitmap/set), so stale entries never render.
- Lost Redis shard: timelines are a cache. Rebuild a missing timeline on read with a pull over followees (slow path, ~200 ms), then write it back. Kafka retention of 7 days allows replaying fanout.
Trade-offs
- Push fanout gives one-read feeds but spends ~1M Redis writes/s and 4 TB of memory; the pull path for celebrities saves that but adds a merge on every read.
- Eventual consistency in feeds (seconds of lag) avoids distributed transactions across the graph, the timelines and the posts; the author gets read-your-writes from their own profile, not the feed.
- Freezing a ranked snapshot gives stable pagination but means new posts don't appear in the middle of a scroll session.
- Immutable content-addressed media makes CDN caching trivial but storage grows; edits create new objects and old ones need garbage collection.
- Storing both follow directions doubles graph storage and needs two writes per follow, in return for fast "who follows me" and "who do I follow" reads.
- Skipping inactive users on fanout saves most of the writes but costs a slow first feed load when they come back.
- Hashtag and location pages are a natural extension: index
PostPublishedevents into an inverted index (tag → post_idsby time), the same pull model as celebrity authors. Ranking is a later scoring layer over the candidate set the timeline already produces.
Interviewer expectations
Mid-level (L4)
- Clear separation of media bytes (object storage + CDN) from metadata (SQL).
- A working feed with fanout on write, and the ability to name the celebrity problem when asked.
- Sensible APIs, with presigned uploads and cursor pagination instead of offsets.
Senior (L5)
- Hybrid fanout with a concrete threshold, activity filtering and the merge on read.
- An asynchronous, idempotent media pipeline with explicit post states and retries.
- Numbers that drive choices: timeline memory, Redis write rate, CDN egress share.
- Correct handling of follow/unfollow backfill and stale entries.
Staff (L6+)
- Stable pagination under ranking (snapshots, new-post hints) and why offsets fail.
- CDN strategy: immutable URLs, tiered caches, signed URLs for private media, storage lifecycle and cost.
- Failure and recovery: timelines as a rebuildable cache, Kafka replay, stuck-media sweepers, multi-region media replication.
- How ranking, discovery and hashtags plug in without redesigning the core write and read paths.