Design a social graph service
Design the service that stores who follows whom for a social network with two billion users: follow and unfollow, lists of followers and following, "do I follow this person?", counts, and mutual connections. Every feed, profile and notification in the product asks it questions, so it must answer in a few milliseconds, even for an account with 150 million followers.
1. The prompt
"Design the follow graph for a social app: users follow each other, see their followers and who they follow, and other services ask the graph questions."
What makes it hard:
- Reads outnumber writes by about 1,000 to 1. Every profile view, feed build and notification checks the graph.
- Each edge has two owners. "Asha follows Ben" must appear in Asha's following list and in Ben's followers list, which live on different machines.
- The degree is wildly uneven. Most people have a few hundred connections; a few accounts have over a hundred million followers.
2. Frame the problem
Follow (one-way) or friend (two-way)?
One-way follows, like most public social apps. A friendship is two follows that must both exist.
One-way edges are simpler to store; mutual relationships become a query (both edges exist).
How big?
2 billion users, 400 million daily actives. An average of 200 following each, so about 400 billion edges.
Far too many for one machine: the graph must be sharded.
What questions do callers ask?
Does A follow B? A's following list, B's followers list (newest first, paginated), the two counts, and mutual follows between two users.
Each question decides an access path. Note what's missing: no arbitrary multi-hop traversals on request.
Read and write volume?
About 2 million graph reads a second at peak, and about 2,000 follows or unfollows a second.
The design is read-optimised; writes can do extra work to make reads cheap.
How fresh must reads be?
A user must see their own follow at once. Others may see it a second or two later.
Read-your-writes for the actor; eventual consistency for everyone else.
Blocks and private accounts?
Yes: a block removes edges both ways and stops new ones; private accounts approve followers.
Pending requests are a separate edge type; blocks are checked on every write.
3. What it must do
- Follow and unfollow, including follow requests for private accounts and blocks.
- Answer point questions fast: does A follow B, and how many followers and followings does someone have.
- Return lists: someone's followers or following, newest first, a page at a time.
- Find mutual follows between two users ("followed by Chen and 12 others you know").
Not now: "people you may know" (an offline job over this graph), the feed itself (D06), ranking of followers.
4. How well it must do it
| Target | Why this number |
|---|---|
| Point reads under 5 ms at p99; list pages under 20 ms speed | Called many times while building one screen. |
| Read your own write immediately; others within a few seconds consistency | Tapping Follow and not seeing it would look broken; a follower count a second behind doesn't. |
| The two directions agree eventually, always correctness | An edge in Asha's list but missing from Ben's must be repaired, not left behind. |
| Availability 99.99% for reads surviving failures | Most of the product can't render without it. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Edges | 2 B users × 200 ≈ 400 B edges; stored twice (both directions) ≈ 800 B rows × ~40 bytes ≈ 32 TB, before replicas and indexes | → so we shard by user ID across a few hundred database machines |
| Reads | 2 M/s, highly skewed toward popular accounts and active users | → so a cache tier answers almost all reads; the databases see only misses and writes |
| Counts | Counting 150 M rows per profile view is impossible | → so counts are stored and updated on each follow, never computed on read |
| The largest account | 150 M followers ≈ 6 GB of follower rows on one shard | One shard holds a celebrity's whole follower list → so lists are always paginated by cursor, and the hottest accounts' counters are split |
Illustrative numbers.
6. The nouns
- User (a node), with a visibility setting.
- Edge: a typed, directed link with a time:
follows,requested,blocks. - Inverse edge: the same link stored on the other user's shard, so each user's lists are local to them.
- Counts per user and edge type.
edge_out (src_id, type, created_at DESC, dst_id) → state sharded by src_id "who does src follow?"
edge_in (dst_id, type, created_at DESC, src_id) → state sharded by dst_id "who follows dst?"
plus a unique index (src_id, type, dst_id) for "does A follow B?"
counts (user_id, type) → following, followers sharded by user_id (split for the largest accounts)
outbox (shard-local) pending inverse writes and count changes
7. The contract
PUT /v1/users/{me}/following/{target} → 200 {state: following | requested}
DELETE /v1/users/{me}/following/{target} → 204
GET /v1/users/{id}/following?cursor=…&limit=50 → {users[], next_cursor}
GET /v1/users/{id}/followers?cursor=…&limit=50 → {users[], next_cursor}
GET /v1/users/{id}/counts → {followers, following}
Internal (other services)
CheckEdges(src, [dst…]) → which of these does src follow? (batched; up to 500 at once)
Mutual(viewer, target, k) → up to k people the viewer follows who also follow the target
Follow and unfollow are idempotent (PUT and DELETE), so a retried tap can't create two edges. Batched checks matter: building a feed of 50 posts asks "do I follow each author?" in one call, not 50.
8. First working version
A graph service in front of a sharded edge store, with a cache tier for reads. Every edge is written twice, once on each user's shard.
8.1 Asha follows Ben
8.2 Reading through the cache
Cache entries are whole small lists and point answers keyed by user (out:asha:follows, count:ben). A write updates the database and then deletes or updates the affected cache entries, so the next read refills them (Module 09).
9. Make it hold up
9.1 Keeping both directions in step correctness
| Approach | Why | |
|---|---|---|
| ✗ Bad | The service writes Asha's shard, then Ben's, and hopes | A crash between the two writes leaves the edge in one list only, for ever. |
| ~ Good | A distributed transaction across both shards (two-phase commit) | Correct, but every follow now waits for two shards and a coordinator, and a slow shard blocks the other. |
| ✓ Great | Write one side plus an outbox row in one local transaction; a relay applies the inverse edge and the count changes, retrying until they succeed. Inverse writes are idempotent (an upsert keyed by both IDs) | The follow commits on one shard, so it's fast. The other side follows within a second and can't be lost. A background checker compares the two directions and repairs any difference. |
Unfollow uses the same path with a delete. If a follow and an unfollow race, compare their timestamps (or a version per edge) and keep the later one on both sides.
9.2 Where does an edge live? speed
9.3 An account with 150 million followers hot spots
- Lists are only ever read a page at a time, with a cursor of
(created_at, src_id), so a request touches 50 rows however big the list is. Nobody scrolls to follower 80 million; cap deep paging. - Counts: 5,000 new followers a second during a live event would make one counter row the hottest thing in the system. Split it into, say, 64 sub-counters updated at random and summed on read (and cached), or batch increments in the relay before writing.
- Reads of hot keys (the celebrity's count and first page) are replicated across many cache servers and kept briefly in each graph service instance's memory.
- Fan-out isn't the graph's job. Delivering one post to 150 M followers is the feed's problem (D06); the graph just lets it page through the follower list.
9.4 Mutual follows speed
"Which of the people I follow also follow this account?" is the intersection of my following list (hundreds) with the target's followers list (possibly millions).
| Approach | Why | |
|---|---|---|
| ✗ Bad | Read both lists in full and intersect them | Reads millions of rows for one line of text on a profile. |
| ~ Good | Read the smaller list, then check each of its members against the larger one | "Does C follow the target?" for each of my 300 followings is 300 point lookups, batched by shard. Fast enough on request. |
| ✓ Great | Probe from the smaller side, stop after k matches, and cache the result per (viewer, target) for a few hours | A profile shows "Chen and 12 others": we need the first few names and a count, not every match. Precise counts for huge lists can come from an offline job. |
Compare the costs in the lab in section 13.
9.5 Regions and reading your own follow consistency
Each shard has one leader region for writes and replicas (database and cache) in every region. Reads are served locally. After Asha follows Ben, her own reads for the next few seconds are routed to the leader (or her region's cache is updated as part of the write), so her button and lists change at once, while other users' views catch up through replication. This is the approach Meta described for TAO: a write-through cache in the leader region, and caches elsewhere refreshed as replication arrives.
9.6 Why not a graph database? architecture
Graph databases (Neo4j, Neptune and others) are excellent at multi-hop questions over a graph that fits a cluster with good locality. A social graph's hot path is different: one hop, enormous volume, uneven degrees, and data that doesn't partition cleanly (any user can follow any other). Large social networks have instead stored adjacency lists in sharded relational or key-value stores with a cache tier in front: Twitter's FlockDB and Meta's TAO both took this shape. Multi-hop work ("friends of friends" for suggestions) runs offline over a snapshot, in a batch or graph-processing system.
9.7 The whole design after the deep dives
10. Interviewer follow-ups
Asha blocks Ben.
One write creates the block edge and, through the same outbox, removes follows in both directions and fixes both users' counts. Follow requests check for a block in either direction on the target's shard before creating an edge.
A bot follows 50,000 accounts an hour.
Rate limits per user on follow writes (with stricter limits for new accounts), and abuse signals that can hide or remove the edges in bulk. Counts shown to users can exclude edges flagged as spam.
Does Ben get notified?
The relay that writes the inverse edge also publishes a Followed event; the notification service subscribes. The graph service doesn't call it directly.
Count and list disagree.
They can, briefly, because counts are updated asynchronously. A periodic job recounts a sample of users (and every large account) from the edge rows and corrects drift.
A user deletes their account.
Mark the user deleted at once (hidden everywhere), then a background job pages through both their lists and removes each edge on both sides, updating the other users' counts. For a celebrity this takes hours, so it must be resumable.
Ten times the users.
More shards (consistent hashing or a directory of shard ranges, so data moves gradually) and more cache servers. Nothing in the design needs a global view.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Edge shard leader down | Follows on that shard fail briefly | Write errors per shard | Replica promoted; reads continue from replicas and cache |
| Outbox relay behind | Followers lists and counts lag | Outbox age | Actor still sees their own follow; relay scales out |
| Cache tier lost in a region | Slower reads; databases flooded | Hit rate, DB load | Request coalescing, rate-limited refill, other cache replicas |
| Directions disagree | An edge missing in one list | Consistency checker | Idempotent repair from the authoritative side |
| Celebrity follow storm | Count updates lag | Hot-key metrics | Split counters, batched increments |
12. Wrap-up and the clock
Trade-offs we made:
- Every edge stored twice, so each user's lists are on their own shard.
- One local write plus an outbox, instead of a distributed transaction; the inverse side catches up within a second.
- Counts stored, not computed; split for the hottest accounts.
- A cache tier answers almost every read; read-your-writes for the actor only.
- One-hop questions online, multi-hop work offline.
What we'd watch: read latency by call type · cache hit rate · outbox age · direction mismatches found by the checker · hot keys · follow write errors and rate-limit rejections.
At 10×: more shards and cache servers; the per-user layout scales linearly. The largest accounts grow faster than the rest, so hot-key handling matters more over time.
A 60-minute interview
| 0–5 | Frame: follow vs friend, size, which questions |
| 5–10 | Must do, how well, numbers |
| 10–15 | Nouns: two edge tables, counts; the contract |
| 15–25 | First version: the follow write and cached reads |
| 25–50 | Deep dives: two directions (9.1), sharding (9.2), celebrities (9.3) |
| 50–60 | Mutual follows or regions, follow-ups, wrap-up |
A 90-minute interview
| 0–8 | Frame, including blocks and private accounts |
| 8–15 | Must do, how well, numbers |
| 15–22 | Nouns, contract |
| 22–35 | First working version |
| 35–75 | Deep dives 9.1–9.6 and the final picture |
| 75–85 | Follow-ups: blocks, bots, deletion |
| 85–90 | Trade-offs, metrics, 10× |
| Level | What a strong answer shows |
|---|---|
| Mid-level | An edges table with the right indexes, follow and unfollow, paginated lists, a cache. |
| Senior | Both directions stored and sharded by owner, an outbox to keep them in step, stored counts, cursor paging, hot keys, mutual follows from the smaller side. |
| Staff+ | Consistency checking and repair as part of the design, read-your-writes across regions, abuse and deletion at scale, why online traversal is avoided and what runs offline instead. |
Common mistakes:
- One edges table sharded one way, so half the queries go to every shard.
COUNT(*)for follower counts.- Two separate writes with no plan for a crash between them.
- Offset pagination over a list of millions.
- Reaching for a graph database without saying which multi-hop query needs it.
13. Try it, check yourself, read more
The mutual follows lab. Set the sizes of the two lists and how many names the profile shows, and compare the ways to find mutual follows.
Illustrative costs: ~1 µs per row scanned in a list, ~0.3 ms per batched round trip of up to 100 point lookups. "Stop after 3" shows the first three names, as a profile does.
How do you serve both "who does Asha follow?" and "who follows Ben?" from single shards?
How do you keep the two copies of an edge in step without a distributed transaction?
Where does a follower count of 150 million come from?
The fastest way to show "followed by Chen and 12 others you know"?
| Resource | Why |
|---|---|
| TAO: Facebook's distributed data store for the social graph (USENIX ATC 2013) | Objects and associations, inverse edges, and a two-tier, multi-region cache. |
| TAO: The power of the graph (Meta Engineering) | A shorter introduction to the same system. |
| FlockDB (Twitter, archived) | Adjacency lists in sharded MySQL, built for huge, shallow graphs. |