Case study
Distributed Social Media Platform
A federated social network built from first principles — multi-node Go backend, Raft consensus for leader election, CRDTs for feed merging, and a timeline fan-out system that stays consistent under network partitions.
Stack
Built as a graduate coursework project at NYU during my Distributed Systems M.S. — the brief was to design and implement a distributed system from scratch. I used it to understand the exact trade-offs production platforms like Twitter/X, Mastodon, and Bluesky make, implementing the hard parts rather than delegating them to a library.
Why
Social platforms look simple from the outside. But once you think about them at scale, every feature is a distributed systems problem in disguise:
- Following is a graph traversal across potentially millions of edges
- Timelines are fan-out write problems — one post, thousands of subscribers
- Likes and counts are concurrent writes that need to converge without coordination
- Feeds need to feel consistent even when nodes disagree
I wanted to build a system where I chose the consistency model deliberately for each part — not just defaulted to “use Postgres for everything.”
Architecture
Cluster coordination — Raft
The cluster runs multiple Go nodes. Leader election and log replication use a Raft implementation written in Go (no external Raft library — that’s the point). Every write that mutates cluster state goes through the Raft log: user creation, follows, post creation. Reads fan out to replicas.
Leader election resolves in under 500ms under normal conditions. On leader crash, a follower wins election within 2–3 heartbeat timeouts. Writes during the election window are rejected with a retriable error — clients retry with exponential backoff.
Posts and timelines — fan-out on write
When a user posts:
- Post written to WAL → replicated via Raft → confirmed to client
- Async fan-out worker reads the author’s follower list from the graph store
- Post ID pushed to each follower’s timeline cache (Redis sorted set, keyed by timestamp)
- Timeline reads hit Redis first; cold reads fall back to the post store
This is fan-out on write — reads are O(1), writes are O(followers). For high-follower accounts (the “celebrity problem”), fan-out switches to pull: timelines for these accounts are assembled at read time and cached with a short TTL.
Counters — CRDTs
Likes, reposts, and view counts are high-write, low-consistency-requirement. Each node maintains a G-Counter CRDT for each post metric. Counters merge via max() — no coordination required. The displayed count is eventually consistent and may lag by seconds, which is the correct trade-off: nobody needs to see the exact like count in real time.
Graph store — follower/following
The social graph (who follows whom) is stored in a separate service backed by an adjacency list in PostgreSQL. Graph reads are cached aggressively in Redis. Writes (follow/unfollow) are idempotent and go through the Raft log for consistency.
Feed ranking
Feeds are reverse-chronological by default. A lightweight scoring pass applies recency decay and a basic engagement signal (likes × recency weight) to re-rank the top N posts before serving. No ML — just arithmetic that I can reason about.
What I verified
- Leader failover: Kill the leader mid-write. Follower wins election within ~2s. In-flight writes that weren’t committed are retried by the client. No data loss.
- Network partition: Split cluster into two halves. Minority partition refuses writes (no quorum). On heal, logs reconcile via Raft. No divergent state.
- Fan-out at scale: Simulated 10K followers per author. Fan-out completes async in < 2s. Timeline reads stay < 10ms (Redis).
- CRDT merge: Two nodes accept concurrent like writes during a partition. Post-heal, counters converge to the correct sum via merge.
What I learned
The hardest part wasn’t Raft. It was the fan-out design — specifically deciding where to draw the consistency boundary. Strong consistency for posts and follows felt right. Eventual consistency for counts felt right. The timeline cache is the interesting middle ground: stale by design, but never wrong.
The second hardest part was the “celebrity problem.” The same fan-out system that works fine at 100 followers breaks at 100,000. Switching to pull for high-follower accounts is a classic solution, but it requires the system to know who the high-follower accounts are and route them differently — which adds operational complexity I didn’t expect from what looked like a simple optimization.
← all work