Requirements & scope
Problem Statement & Requirements
Design a social graph system that models relationships between users and powers core social features — friend/connection management, "People You May Know" suggestions, mutual connections, degree-of-separation queries, and graph-based feed ranking — similar to LinkedIn's connection graph, Facebook's social graph, or Twitter's follower graph.
Functional Requirements
- Add / remove connection — send connection request, accept/reject, remove existing connection
- Follow / unfollow — asymmetric follow relationship (Twitter model) alongside symmetric connections (LinkedIn/Facebook model)
- Get connections — list a user's 1st-degree connections with pagination
- Mutual connections — "You and Alice have 12 mutual connections"
- Degree of separation — "Alice is a 2nd-degree connection" (friend of a friend)
- People You May Know (PYMK) — recommend new connections based on graph proximity, shared attributes, mutual connections
- Connection count — display connection/follower/following counts
- Graph search — "Find people named Bob who work at Google and are connected to Alice"
- Privacy controls — control who can see your connections, who can send requests
- Block user — hide from each other's graph entirely
Non-Functional Requirements
- Low latency — connection lookup < 10 ms; mutual connections < 50 ms; PYMK < 200 ms
- High read throughput — 500K+ graph queries/sec (connections list, mutual, PYMK)
- Consistency — connection state must be strongly consistent (if I connect, both parties see it immediately)
- Scale — 1B+ users, 500B+ edges (connections + follows)
- Availability — 99.99% for read operations
- Efficient traversal — 2nd and 3rd degree queries must be fast despite billions of edges
Out of Scope
- News feed generation (see News Feed design)
- Messaging system (see Chat System design)
- Profile / content management
- Full knowledge graph (entity relationships beyond people)
Scale estimations
Scale Estimations
Traffic
| Metric | Value |
|---|---|
| Total users | 1B |
| Monthly active users | 500M |
| Average connections per user | 500 (LinkedIn avg ~930 for active users) |
| Total edges (bidirectional connections) | 1B × 500 / 2 = 250B edges |
| Follow edges (asymmetric) | 250B additional edges |
| Total graph edges | ~500B |
| Connection list reads / second | 200K RPS |
| Mutual connection queries / second | 100K RPS |
| PYMK queries / second | 50K RPS |
| Connection requests / second | 5K RPS (writes) |
| Graph search queries / second | 30K RPS |
| Total graph queries / second | ~400K RPS |
Storage
| Metric | Value |
|---|---|
| Edge record size | 32 bytes (user_id_a: 8B, user_id_b: 8B, type: 1B, created_at: 8B, metadata: 7B) |
| Total edge storage | 500B × 32 B = ~16 TB |
| Adjacency list (per user, avg 500 connections) | 500 × 8 B = 4 KB per user |
| Total adjacency lists | 1B × 4 KB = ~4 TB |
| User profile index for graph search | 1B × 200 B = ~200 GB |
| PYMK precomputed suggestions cache | 500M MAU × 1 KB = ~500 GB |
Bandwidth
| Metric | Value |
|---|---|
| Connection list responses (200K/s × 5 KB) | ~1 GB/s |
| Mutual connection queries (100K/s × 500 B) | ~50 MB/s |
| PYMK responses (50K/s × 2 KB) | ~100 MB/s |
| Total read bandwidth | ~1.2 GB/s |
Hardware Estimate
| Component | Spec |
|---|---|
| Graph store (adjacency lists) | 50-100 nodes (in-memory sharded) |
| Edge store (persistent) | 30-50 shards (SSD-backed) |
| PYMK computation cluster | 20-50 nodes (batch + incremental) |
| Cache (Redis) | 20-50 nodes (hot user adjacency lists) |
| Graph search index (Elasticsearch) | 30-50 nodes |
Layered architecture
High-Level Architecture
High-Level ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Component Breakdown
Component BreakdownExcalidraw diagram · editable shapes · reveal step by stepExplore
API & contracts
API Design
Send Connection Request
POST /api/v1/connections/request
Authorization: Bearer <token>
Request:
{
"target_user_id": "user-bob-456",
"message": "Hi Bob, we met at the conference!" // optional
}
Response (201 Created):
{
"request_id": "req-abc123",
"status": "pending",
"from": "user-alice-123",
"to": "user-bob-456",
"created_at": "2026-04-03T10:00:00Z"
}Accept / Reject Connection
POST /api/v1/connections/request/{request_id}/accept
Response (200 OK):
{
"connection_id": "conn-xyz789",
"users": ["user-alice-123", "user-bob-456"],
"connected_at": "2026-04-03T10:05:00Z"
}
POST /api/v1/connections/request/{request_id}/reject
Response: 204 No ContentGet Connections (Adjacency List)
GET /api/v1/users/{user_id}/connections?limit=20&cursor=eyJsYXN0IjoiY29ubl8xMjM0NTY3In0=
Response (200 OK):
{
"user_id": "user-alice-123",
"total_connections": 847,
"connections": [
{
"user_id": "user-bob-456",
"name": "Bob Smith",
"headline": "VP Engineering at Acme Corp",
"profile_image": "https://cdn.example.com/bob.jpg",
"connected_at": "2026-04-03T10:05:00Z",
"mutual_connection_count": 12
},
...
],
"next_cursor": "eyJsYXN0IjoiY29ubl85OTk5In0="
}Get Mutual Connections
GET /api/v1/users/{user_id_a}/mutual/{user_id_b}?limit=10
Response (200 OK):
{
"user_a": "user-alice-123",
"user_b": "user-charlie-789",
"mutual_count": 12,
"mutual_connections": [
{
"user_id": "user-bob-456",
"name": "Bob Smith",
"headline": "VP Engineering at Acme Corp"
},
...
]
}Get Degree of Separation
GET /api/v1/users/{user_id_a}/degree/{user_id_b}
Response (200 OK):
{
"user_a": "user-alice-123",
"user_b": "user-dave-999",
"degree": 2,
"path": [
"user-alice-123", // Alice
"user-bob-456", // → Bob (1st degree)
"user-dave-999" // → Dave (2nd degree via Bob)
]
}People You May Know
GET /api/v1/users/{user_id}/pymk?limit=20
Response (200 OK):
{
"suggestions": [
{
"user_id": "user-eve-111",
"name": "Eve Johnson",
"headline": "Data Scientist at BigCorp",
"mutual_connections": 8,
"shared_company": "Acme Corp",
"shared_school": null,
"score": 0.87,
"reason": "8 mutual connections"
},
...
]
}Graph Search
GET /api/v1/search/people?q=product+manager
&company=Google
&location=San+Francisco
°ree=2
&limit=20
Response (200 OK):
{
"total": 342,
"results": [
{
"user_id": "user-frank-222",
"name": "Frank Lee",
"headline": "Sr. Product Manager at Google",
"location": "San Francisco, CA",
"degree": 2,
"mutual_connections": 5,
"relevance_score": 0.92
},
...
]
}Data model
Data Model
Edge Table (Persistent Store)
Edge Table (Persistent Store)Excalidraw diagram · editable shapes · reveal step by stepExplore
Adjacency List (In-Memory Index)
Adjacency List (In-Memory Index)Excalidraw diagram · editable shapes · reveal step by stepExplore
Connection Request
Connection RequestExcalidraw diagram · editable shapes · reveal step by stepExplore
Core design decisions
Core Design Decisions
Decision 1: Graph Storage — How to Store 500B Edges
Option A: Relational Database (MySQL/PostgreSQL)
Option A: Relational Database (MySQL/PostgreSQL)Excalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| Familiar, well-understood | Joins for mutual connections = slow at scale |
| ACID transactions | Multi-hop traversals (2nd, 3rd degree) = expensive |
| Easy sharding by user_id | Shard-crossing for reverse lookups |
| Indexes handle single-hop well | Fan-out for 2nd degree: 500 × 500 = 250K lookups |
Option B: Native Graph Database (Neo4j / JanusGraph)
Option B: Native Graph Database (Neo4j / JanusGraph)Excalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| O(1) per hop traversal (index-free adjacency) | Harder to shard (graph partitioning is NP-hard) |
| Natural query language for graph patterns | Less mature for 500B+ edges at write throughput |
| Multi-hop queries are first-class | Operational complexity (less tooling than SQL) |
| Beautiful for 2nd/3rd degree, PYMK | Expensive at massive scale (licensing/infra) |
Option C: Adjacency List in KV Store + SQL Edge Table (LinkedIn/Facebook Approach)
Option C: Adjacency List in KV Store + SQL Edge Table (LinkedIn/Facebook Approach)Excalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| Sub-ms reads from memory | Two stores to keep in sync |
| SQL durability for writes | Memory cost (~4 TB for full graph) |
| Set intersection for mutuals is trivial | Rebuilding adjacency list from edge table on miss |
| Proven at Facebook/LinkedIn scale | More infrastructure complexity |
Recommendation: Option C (Adjacency in Memory + SQL Edges)
This is what Facebook (TAO) and LinkedIn (follow graph) actually use.
In-memory adjacency for reads (hot path), SQL for writes (durability).
Graph DBs are great for exploration queries but harder to shard at 500B edges.Decision 2: Mutual Connection Computation
Decision 2: Mutual Connection ComputationExcalidraw diagram · editable shapes · reveal step by stepExplore
Decision 3: Degree of Separation — BFS on Billion-Node Graph
Decision 3: Degree of Separation — BFS on Billion-Node GraphExcalidraw diagram · editable shapes · reveal step by stepExplore
Request flows
Detailed Flow Diagrams
Connection Request → Accept Flow
Connection Request → Accept FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Mutual Connections Query
Mutual Connections QueryExcalidraw diagram · editable shapes · reveal step by stepExplore
PYMK Batch Computation
PYMK Batch ComputationExcalidraw diagram · editable shapes · reveal step by stepExplore
Performance & caching
Workshop note · added for the website’s common reading format
Start with the dominant access pattern of Social Graph. Adjacency cache is one place to inspect capacity and tail latency. Measure before introducing a cache: define the cache key, invalidation policy, stale-data budget, and cold-start behavior.
Batch independent work where the latency budget allows it. Bound queues and concurrency, and verify that an optimization does not move the bottleneck to a dependency.
Advanced design
Graph Search
Graph SearchExcalidraw diagram · editable shapes · reveal step by stepExplore
Edge cases
Handling Edge Cases
Celebrity / High Fan-Out Users
Problem: Elon Musk has 150M followers
→ Adjacency list: 150M × 8 bytes = 1.2 GB for ONE user
→ Mutual connections with 150M: sort-merge = very expensive
Solution: Tiered Storage
Normal users (< 50K connections):
→ Full adjacency list in memory (standard path)
→ Real-time set intersection for mutuals
High fan-out users (> 50K followers):
→ Followers stored in persistent store only (not in-memory)
→ Follower count maintained as counter (not list length)
→ Mutual connections: use bloom filter approximation
→ "~23 mutual connections" (approximate, not exact list)
→ Exact mutual list computed on-demand (paginated, not all at once)
Bidirectional connections (LinkedIn-style) naturally bounded:
→ Max ~30K connections on LinkedIn
→ Always fits in memory
→ Followers (asymmetric) can be unlimited → tieredConsistency on Connection Create
Problem: Alice connects to Bob
→ Must update: edge table, Alice's adjacency, Bob's adjacency
→ If Alice's adjacency updates but Bob's fails → inconsistent
Solution: Event-Sourced Writes
1. Write to edge table (source of truth) — atomic DB transaction
2. Publish "connection_created" event to Kafka
3. Adjacency list updater consumes event:
→ Update Alice's list (retry on failure)
→ Update Bob's list (retry on failure)
4. If adjacency update fails:
→ Consumer retries (Kafka guarantees at-least-once)
→ Adjacency store is eventually consistent (seconds lag OK)
5. Worst case: adjacency stale → user refreshes → reads from edge table
Critical: Edge table write is the commit point.
Adjacency lists are derived views (can always be rebuilt).Graph Partitioning (Sharding)
Graph Partitioning (Sharding)Excalidraw diagram · editable shapes · reveal step by stepExplore
Blocking and Privacy
Block user:
1. INSERT edge (A, B, type=blocked)
2. Remove connection edge if exists: DELETE (A,B) and (B,A)
3. Update adjacency lists: remove from both
4. Block record checked on ALL graph operations:
→ A never appears in B's search results, PYMK, or mutual lists
→ B never appears in A's search results, PYMK, or mutual lists
5. Bloom filter of blocked pairs checked before any result returned
→ Fast O(1) check, minimal false positives
Privacy settings:
"Only connections can see my connections list"
→ Graph Query Service checks viewer's relation to target
→ If viewer is not connected AND target's setting = private:
→ Return: "Connection list is private" (not the data)
→ Applied at API layer, not storage layerTradeoffs
Tradeoffs & Design Decisions Summary
| Decision | Option A | Option B | Chosen | Why |
|---|---|---|---|---|
| Graph storage | SQL edge table only | Graph DB (Neo4j) | Hybrid: in-memory adjacency + SQL edges | In-memory for sub-ms reads; SQL for durable writes; proven at LinkedIn/FB scale |
| Adjacency representation | On-disk (B-tree index) | In-memory sorted arrays | In-memory | < 0.5 ms reads; 4 TB fits in cluster; the graph IS the cache |
| Mutual connections | SQL JOIN | In-memory set intersection | In-memory merge-intersect | O(n+m) on sorted arrays = microseconds; SQL join on 500B row table = seconds |
| Degree of separation | Unidirectional BFS | Bidirectional BFS | Bidirectional + sampling | 5,700× faster than unidirectional; sampling keeps depth-3 practical |
| PYMK | Real-time per request | Batch + incremental updates | Batch nightly + incremental | Real-time for 500M users = infeasible; batch computes, events update incrementally |
| Edge storage | Single row per connection | Two rows (A→B, B→A) | Two rows | 2× storage but each user's adjacency scan is a simple prefix query (no reverse index needed) |
| Sharding | Social-cluster partitioning | Hash-based | Hash + caching | Hash is simple, even; caching adjacency lists eliminates cross-shard traversal penalty |
| High fan-out | Full adjacency in memory | Tiered (memory for connections, disk for followers) | Tiered | 150M-follower users can't fit in memory; bloom filter for approximate mutuals |
| Graph search | Graph DB native | Elasticsearch + graph overlay | ES + graph boosting | ES proven for text search at scale; graph annotations added as re-ranking layer |
| Consistency | Synchronous dual-write | Event-sourced (write edges → event → update adjacency) | Event-sourced | Edge table is commit point; adjacency is derived, eventually consistent (seconds) |
Reliability & fault tolerance
Reliability & Fault Tolerance
Single Points of Failure & Mitigations
Single Points of Failure & MitigationsExcalidraw diagram · editable shapes · reveal step by stepExplore
Graceful Degradation
Tier 1 (PYMK service down):
→ Profile page shows no suggestions
→ Core connections/search unaffected
→ PYMK rebuilds when service recovers
Tier 2 (Graph search degraded):
→ Fall back to text-only search (no graph ranking)
→ Results still useful, just not personalized by degree
→ "N mutual connections" badge missing
Tier 3 (Adjacency store shard down):
→ Connections for affected users fetched from edge table (slower, ~20 ms)
→ Read replica of adjacency store serves reads if available
→ Mutual connections degrade to approximate (bloom filter) or unavailable
Tier 4 (Edge table shard down):
→ Cannot create new connections (writes fail)
→ Existing adjacency lists still serve reads (in-memory)
→ Auto-failover to standby (< 30s)Production architecture
Full System Architecture (Production-Grade)
Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
Graph Algorithms Used in Production
Graph Algorithms Used in ProductionExcalidraw diagram · editable shapes · reveal step by stepExplore
Further exploration
Facebook TAO — Reference Architecture
Facebook TAO — Reference ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Interview playbook
Interview Tips
Start with the data model: two rows per bidirectional connection — Store edges (A→B) and (B→A). 2× storage but each user's connection list is a simple prefix scan. This is the non-obvious insight that shows you've thought deeply about read patterns.
In-memory adjacency lists are the key performance decision — "500B edges × 32 bytes = 16 TB on disk. But adjacency lists (sorted user_id arrays) = 4 TB, which fits in a 100-node cluster's RAM. Sub-millisecond reads, set intersection for mutuals in microseconds." This is how Facebook TAO and LinkedIn actually work.
Mutual connections = set intersection, not SQL JOIN — Load A's sorted array and B's sorted array. Merge-intersect in O(n+m). For 500 + 800 connections = ~1,300 comparisons = microseconds. SQL JOIN on a 500B-row table would take seconds.
Bidirectional BFS for degree of separation — Don't do naive BFS from one end. Expand from BOTH endpoints, meet in middle. O(b^(d/2)) vs O(b^d) = 5,700× faster for depth 3 with branching factor 500. Limit to 3rd degree (LinkedIn does "3rd+").
PYMK is the hardest problem — Can't compute in real-time for 500M users. Explain: nightly batch job (Spark, ~5 hours) computes 2nd-degree candidates, scores by mutual count + shared attributes. Incremental updates via Kafka events when connections change. Serve from Redis cache.
Celebrity/influencer problem — Normal users have ~500 connections (fits in memory). Elon has 150M followers (1.2 GB). Solution: tiered storage — connections in memory, follower lists on disk. Approximate mutual count via bloom filters for high fan-out users.
Graph sharding is hard — Graph partitioning is NP-hard. Don't try social-cluster partitioning. Use hash sharding (simple, even) and aggressive caching of adjacency lists. The adjacency list IS the cache — 4 TB fits in memory.
Event-sourced writes for consistency — Edge table write is the commit point (ACID). Adjacency list updates are derived via Kafka events (eventually consistent, seconds lag). If adjacency store fails, edges still safe in DB; adjacency rebuilt.
Graph search = Elasticsearch + graph re-ranking — Don't build a graph-native search engine. Use ES for text/attribute search, then re-rank results by graph distance (1st degree = boost 3×, 2nd degree = boost 2×, mutual count). Two systems, each doing what it does best.
Reference Facebook TAO — "Facebook TAO: in-memory cache (99.8% hit rate, < 1 ms reads) backed by sharded MySQL. Billions of reads/sec. Writes go to MySQL first, then invalidate cache. Scale reads by adding cache nodes. This is the architecture I'm following." Instant credibility.