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.

Shipped GitHub ↗
GoRaftgRPCPostgreSQLRedisDockerKubernetes

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:

  1. Post written to WAL → replicated via Raft → confirmed to client
  2. Async fan-out worker reads the author’s follower list from the graph store
  3. Post ID pushed to each follower’s timeline cache (Redis sorted set, keyed by timestamp)
  4. 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