Building Raft, and everything I did not build

Dummy page. The prose below is placeholder trunk narrative so the tabs have something to link to; the tree and timeline are live from the project’s decision log.

Ports first

I started with the boundary, not the algorithm. Three interfaces in raft/ports.go: Transport, Storage, StateMachine. Everything that touches a network, a disk, or an application lives behind one of them, and the core never imports anything else.

One goroutine owns the state

The first three real commits after the scaffold were each a concurrency fix: a deadlock, a race on the role field, a ticker race. The lesson landed the way it lands for everyone: serialize state changes through a single event loop and let workers signal it over channels.

It is a portfolio piece, and that is fine

After a three-month gap I spent a day trying to find a use case that demanded Raft at the scale of one person’s hardware. There isn’t one. Raft exists because of scale, adversaries, or geography, and none of those shrink to a single owner.

Testing: fakes do the work, real chaos calibrates them

Deterministic simulation is the primary explorer. Property tests sit alongside it. Real-network chaos comes later and is used to check the fakes, not to find bugs first.

Three bugs, three commits

Before changing any architecture: the election timeout is re-randomized at construction, at each election, and on step-down. A dead field went. AppendEntries handles its storage errors with a rule I want to keep everywhere: a read that fails means the log has a shape we did not expect, so return Success:false; a write that fails would silently violate Log Matching, so panic.

The pivot: a passive core

Pull every goroutine out of raft/. The node becomes a struct with five methods (Tick, Step, Propose, Ready, Advance) and whoever calls them decides when.

Unstable entries, and a reversal

For a few hours the node wrote log entries straight to storage inside Step. Then I reversed it: new entries wait in an in-memory buffer, Ready exposes them, the runner persists them and calls Advance. The node performs zero I/O.

type Ready struct {
    HardState        *HardState
    Entries          []LogEntry
    CommittedEntries []LogEntry
    Messages         []Message
}

How this page is built

Every decision, adopted or not, is logged to a local DECISIONS.md as it happens. This write-up follows the trunk; the tree tab holds every branch; the timeline is both by date. They are three renderings of one data file.