Built a distributed key-value store from scratch to actually understand how Raft works. It's a 3-node cluster running in Docker, talking over gRPC.
Clients use consistent hashing locally to pick a node. If they accidentally hit a follower, the follower just hands back a redirect to the current leader.
Client → consistent-hash ring → pick node → gRPC (Get/Put/Delete)
│
┌──────┴──────┐
│ KV Server │
└──────┬──────┘
│
┌──────┴──────┐
│ Raft Node │ ◄──── AppendEntries / RequestVote
└──────┬──────┘
│ ApplyChannel
┌──────┴──────┐
│ KV Store │
└─────────────┘
You just need Docker, Compose, and Go 1.22+.
1. Boot the cluster
docker-compose up --build
Nodes find each other via Docker DNS. Leader election happens in about 300ms.
2. Play with the CLI
go build -o client ./cmd/client
./client put mykey myvalue
./client get mykey
./client delete mykey
The best way to see Raft working is to break things.
# Find out who the leader is
docker-compose logs | grep "became leader"
# Kill that container (say it's node1)
docker kill raftkv-node1-1
# Watch the other two panic for a split second, then elect a new leader
docker-compose logs -f node2 node3
# Writes still work perfectly
./client put testkey survived_the_crash
# Turn the old leader back on. It wakes up as a follower and syncs the missing logs.
docker start raftkv-node1-1
# Fast in-memory unit tests (no Docker needed)
go test ./test/... -v -timeout 30s
# Hardcore fault injection (needs the Docker cluster running)
go test ./test/... -run TestFault -v -timeout 120s
# Benchmarks
go test ./bench/... -bench=. -benchtime=30s -v
On my 4-core laptop, hitting the 3-node cluster with 8 parallel writers gets me about 6,400 ops/sec. Failover takes around 240ms.
I tried to stick to the Raft paper as much as possible, but took a few shortcuts to keep the code readable.
What I actually implemented:
- Standard leader election with randomized timeouts (150-300ms).
- Strict log matching (
prevLogIndex/prevLogTerm) and truncation. - New-leader no-op entries.
- ReadIndex for linearizable reads without hitting the disk every time.
- Snapshots, log compaction, and accelerated backtracking for conflicts.
Where I cheated:
- I share one gRPC port for both client traffic and internal Raft chatter. It just makes Docker networking way easier for a demo.
- Logs are JSON and I only fsync on snapshots. Fsyncing every single append is mathematically correct but brutally slow for a side project.
- Static membership. No joint consensus or adding/removing nodes on the fly. That's a massive headache for another day.