Skip to content
Engineering

Engineering · 2025

Raft Bare Bones

A teaching-first implementation of the Raft consensus algorithm in Go, with readable election and replication logic, an HTTP transport, and a small replicated key/value application.

Minimal three-node consensus mark above two raft lines.

Why I built it

Raft is often introduced as the understandable consensus algorithm. The paper is unusually clear, but reading an algorithm and implementing its state transitions are different kinds of understanding.

I built Raft Bare Bones to test whether I understood the algorithm well enough to make it work—and then explain it through code. The goal was not to create another production-grade Raft library. It was to build one small enough to read in a sitting, trace end to end, and modify without first navigating layers of optimisation and defensive machinery.

That choice shaped the project: clarity over throughput, explicit interfaces over clever abstractions, and enough real I/O to observe consensus rather than only simulate it in tests.

What I built

Raft Bare Bones is a minimal Go implementation of leader election and replicated logs. It includes the consensus core, JSON-over-HTTP transport, an in-memory log, and a key/value application that uses the replicated log as its source of truth.

The repository is split by responsibility:

  • core contains elections, replication, timers, node state, and the interfaces for storage, transport, and application state;
  • transport/httpserver and transport/httpclient expose RequestVote and AppendEntries as ordinary HTTP requests;
  • apps/kv implements a small deterministic finite-state machine and its HTTP handlers;
  • cmd/rbb wires a node, transport, storage, and application into a runnable process;
  • logging keeps role, term, election, and commit transitions visible through structured slog output;
  • scripts contains the three-node local-cluster helper.

Keeping networking and application behaviour outside the consensus state machine was an important design constraint. The core package can reason about peers, terms, votes, log entries, and commits without knowing whether messages travel over HTTP or what an applied command means to the application.

Following a write through the cluster

Applications submit commands through node.Propose. In the example key/value store, an HTTP request is encoded into a log entry and proposed to the local node.

Only the leader can accept that proposal. A follower returns not leader, which the HTTP layer exposes as a 409, so a client can redirect or retry against the current leader.

The leader appends the command to its log and replicates it to peers through AppendEntries. Once a majority has accepted the entry, the commit index can advance and the command is applied to the finite-state machine. Each node applies the same committed commands in the same order, producing the same key/value state.

The implementation keeps this path deliberately visible. The transport uses JSON, and meaningful transitions are logged, so election starts, role changes, replication, and commit-index movement can be followed without specialised observability tooling.

Watching consensus happen

The included script starts three processes on ports 70017003:

make run-3node

After a leader is elected, writes can be sent to that node and read from the others after replication:

curl -X POST http://127.0.0.1:7001/kv/consensus -d 'raft'
curl http://127.0.0.1:7002/kv/consensus

Killing the leader makes the mechanics concrete: the remaining nodes time out, start an election, choose a new leader, and continue accepting writes. Restarting the old process lets it catch up from the replicated log.

The demo was useful because it turned abstract properties—majority agreement, leader authority, ordered application, and failover—into behaviour I could inspect one request and log line at a time.

Extending it with another application

The key/value store is only an example. A different application implements core.FSM, encodes its commands into core.LogEntry.Data, and applies committed entries deterministically in Apply.

After attaching that FSM to a node, the application only needs thin I/O glue: domain-specific handlers prepare commands for node.Propose, while transport errors such as ErrNotLeader and ErrNoTransport become useful client responses.

The repository includes a step-by-step replicated-counter guide to demonstrate this boundary with something smaller than the key/value service.

Deliberate limitations

This is a learning scaffold, not a production consensus library. Logs currently live in memory, cluster membership is static, and the implementation does not include snapshots, membership changes, read-index support, or replication batching and pipelining.

Those omissions are intentional. Production Raft implementations need extensive validation, durable storage semantics, recovery handling, performance work, and many edge-case guards. Adding all of that would make this project less useful for its original purpose: seeing the core algorithm clearly.

The architecture guide explains the package boundaries and write path in more detail, while the roadmap keeps the missing capabilities explicit.

Begin a conversation

Get in touch.