01

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)
02

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
03

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
04

API & contracts

API Design

Send Connection Request

EXAMPLE
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

EXAMPLE
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 Content

Get Connections (Adjacency List)

EXAMPLE
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

EXAMPLE
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

EXAMPLE
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

EXAMPLE
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

EXAMPLE
GET /api/v1/search/people?q=product+manager
    &company=Google
    &location=San+Francisco
    &degree=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
    },
    ...
  ]
}
05

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
06

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)
EXAMPLE
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
07

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
08

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.

09

Advanced design

Graph Search

Graph SearchExcalidraw diagram · editable shapes · reveal step by stepExplore
10

Edge cases

Handling Edge Cases

Celebrity / High Fan-Out Users

EXAMPLE
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 → tiered

Consistency on Connection Create

EXAMPLE
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

EXAMPLE
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 layer
11

Tradeoffs

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)
12

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

EXAMPLE
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)
13

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
14

Further exploration

Facebook TAO — Reference Architecture

Facebook TAO — Reference ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
15

Interview playbook

Interview Tips

  1. 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.

  2. 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.

  3. 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.

  4. 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+").

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  9. 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.

  10. 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.