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
Phase 1 of 4 · Client & edge
Start with the entry points
First, define who uses the system and where requests enter. This is the boundary of the design, not yet a complete request path.
- Client
- Distinguish directed follows from mutual connections and define visibility rules.
- API gateway
- Authorize relationship reads as well as writes; graph reachability does not imply permission.
This first layer defines the system boundary. The next chapters add application logic and data dependencies. After examining the request paths and tradeoffs, we will add operational safeguards and bring the complete architecture together.
API & contracts
Phase 2 of 4 · Application & compute
Add the application logic
Keep the entry points and add the services that do the work. Give each service a clear responsibility before defining its contracts. Storage and background dependencies come next.
- Connection service
- Create or remove relationship edges with conditional state changes and stable request IDs.
- Graph query service
- Use bounded traversals and adjacency intersections rather than unbounded graph scans.
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
Phase 3 of 4 · Data & async work
Connect data and background work
The services now need their dependencies: durable state, fast lookups, or work that continues outside a request. Separate these responsibilities rather than treating every component as a database.
- Adjacency cache
- Keep hot neighbor lists close to the query path while bounding cache staleness.
- Edge store
- Persist relationships independently of derived counts and caches.
- Suggestion engine
- Generate candidates offline, then rerank with current privacy and block constraints.
Data Model
Edge Table (Persistent Store)
Adjacency List (In-Memory Index)
Connection Request
Core design decisions
Core Design Decisions
Decision 1: Graph Storage — How to Store 500B Edges
Option A: Relational Database (MySQL/PostgreSQL)
| 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)
| 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)
| 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 3: Degree of Separation — BFS on Billion-Node Graph
Request flows
Detailed Flow Diagrams
Connection Request → Accept Flow
Mutual Connections Query
PYMK Batch Computation
Performance & caching
Neighbor-list caches make common graph reads cheap, but very high-degree users can still dominate memory and intersection work. Fetch bounded subsets where the product allows it and choose intersection algorithms suited to list sizes. Recheck privacy and block rules before displaying a cached relationship or suggestion.
Advanced design
Graph Search
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)
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
Phase 4 of 4 · Operations & resilience
Add operational safeguards
With the request paths and tradeoffs established, add the controls that help detect trouble and keep the system operating. The final diagram retains every layer introduced so far.
- Event bus
- Publish graph changes so caches, counts, and suggestions can converge and be rebuilt.
Reliability & Fault Tolerance
Single Points of Failure & Mitigations
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
High-Level Architecture
Component Breakdown
Full System Architecture (Production-Grade)
Graph Algorithms Used in Production
Further exploration
Facebook TAO — Reference Architecture
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.