DEV Community

Cover image for Distributed Systems: Implementing the Raft Consensus Protocol from Scratch
Ebendttl
Ebendttl

Posted on • Originally published at akinseinde.netlify.app

Distributed Systems: Implementing the Raft Consensus Protocol from Scratch

This is an excerpt. The full article includes a live interactive Raft consensus cluster simulator — control a 5-node distributed cluster, trigger leader elections, partition nodes to simulate network splits, commit client state updates, and watch the term-clock and replication logs resolve conflicts in real time. Read the full interactive version →


The Distributed State Problem

How do you get multiple independent computer nodes to agree on a single sequence of events, especially when individual nodes can crash, experience network delays, or drop packets?

This is the core challenge of Distributed Consensus. In a distributed database, all replicas must execute incoming commands in identical order to maintain a consistent state.

For years, the industry standard was Paxos — an algorithm famously powerful but incredibly complex to understand and implement without error.

Raft was designed as an alternative. It breaks down the consensus problem into three modular sub-problems: Leader Election, Log Replication, and Safety Invariants.


The Three Raft Node States

At any given moment, every node in a Raft cluster operates in one of three distinct roles:

        ┌───────────────────────────────────────┐
        │                FOLLOWER               │
        └───────────────────────────────────────┘
          │ (Times out, starts election)   ▲
          ▼                                │ (Discovers current leader)
        ┌───────────────────────────────────────┐
        │               CANDIDATE               │
        └───────────────────────────────────────┘
          │ (Receives majority votes)      
          ▼                                
        ┌───────────────────────────────────────┐
        │                 LEADER                │
        └───────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode
  1. Follower: Completely passive. Responds to incoming RPC calls from Candidates and Leaders. If a follower receives no communication for a randomized timeout period, it assumes the leader has crashed and transitions to a Candidate.
  2. Candidate: Increments the cluster "term", votes for itself, and broadcasts requests for votes to all other nodes.
  3. Leader: Manages all client writes. Coordinates log replication and broadcasts periodic "heartbeat" RPCs to assert authority and reset follower timeout clocks.

Sub-Problem 1: Leader Election

To prevent split votes, followers wait for a randomized election timeout (typically between 150ms and 300ms) before initiating an election.

Once a node's timeout expires, it:

  1. Increments its local currentTerm counter.
  2. Transitions to the Candidate state.
  3. Votes for itself.
  4. Sends a RequestVote RPC to all other nodes.

If a candidate receives votes from a majority of nodes in the cluster (e.g., 3 out of 5), it immediately transitions to Leader and starts broadcasting heartbeats.


Sub-Problem 2: Log Replication

Once a Leader is elected, it acts as the single gateway for all client write requests.

Client ─── Write "X = 5" ───> Leader
                                │
          ┌─────────────────────┴─────────────────────┐
          │ (Append entry, broadcast AppendEntries)   │
          ▼                                           ▼
      Follower A                                  Follower B
Enter fullscreen mode Exit fullscreen mode

The Replication Steps:

  1. The client sends a command (e.g., set x=5) to the Leader.
  2. The Leader appends the command to its local log.
  3. The Leader broadcasts an AppendEntries RPC containing the new log entry to all followers.
  4. Followers verify the entry and append it to their logs, sending a success confirmation back.
  5. Once a majority of followers acknowledge the write, the Leader commits the entry to its state machine and returns a success response to the client.
  6. In subsequent heartbeats, the Leader notifies followers of the newly committed entry, prompting them to commit it to their local state machines.

TypeScript Raft Node Implementation

Here is the baseline state container and interface schema for a Raft consensus node in TypeScript:

type NodeRole = "Follower" | "Candidate" | "Leader";

interface LogEntry {
  term: number;
  command: string;
}

interface RequestVoteArgs {
  term: number;
  candidateId: string;
  lastLogIndex: number;
  lastLogTerm: number;
}

interface RequestVoteReply {
  term: number;
  voteGranted: boolean;
}

class RaftNode {
  public id: string;
  public currentTerm = 0;
  public role: NodeRole = "Follower";
  public votedFor: string | null = null;
  public log: LogEntry[] = [];

  private commitIndex = 0;
  private electionTimeout: NodeJS.Timeout | null = null;
  private peers: string[] = [];

  constructor(id: string, peers: string[]) {
    this.id = id;
    this.peers = peers;
    this.resetElectionTimeout();
  }

  private resetElectionTimeout() {
    if (this.electionTimeout) clearTimeout(this.electionTimeout);

    // Randomized timeout between 150ms and 300ms prevents split votes
    const timeout = 150 + Math.floor(Math.random() * 150);
    this.electionTimeout = setTimeout(() => this.startElection(), timeout);
  }

  // Handle incoming vote request from Candidate node
  public handleRequestVote(args: RequestVoteArgs): RequestVoteReply {
    this.resetElectionTimeout();

    // 1. Term check: reject candidates with stale terms
    if (args.term < this.currentTerm) {
      return { term: this.currentTerm, voteGranted: false };
    }

    if (args.term > this.currentTerm) {
      this.currentTerm = args.term;
      this.role = "Follower";
      this.votedFor = null;
    }

    // 2. Log completeness check: candidate log must be at least as up-to-date as ours
    const lastIndex = this.log.length - 1;
    const lastTerm = lastIndex >= 0 ? this.log[lastIndex].term : 0;
    const logIsUpToDate =
      args.lastLogTerm > lastTerm ||
      (args.lastLogTerm === lastTerm && args.lastLogIndex >= lastIndex);

    // 3. Vote check
    const canVote = this.votedFor === null || this.votedFor === args.candidateId;

    if (canVote && logIsUpToDate) {
      this.votedFor = args.candidateId;
      return { term: this.currentTerm, voteGranted: true };
    }

    return { term: this.currentTerm, voteGranted: false };
  }

  private startElection() {
    this.role = "Candidate";
    this.currentTerm++;
    this.votedFor = this.id;
    this.resetElectionTimeout();

    console.log(`Node ${this.id} starting election for Term ${this.currentTerm}`);

    let votesReceived = 1; // Vote for self
    const lastLogIndex = this.log.length - 1;
    const lastLogTerm = lastLogIndex >= 0 ? this.log[lastLogIndex].term : 0;

    for (const peerId of this.peers) {
      // Simulate dispatching network RPC calls to peer cluster
      this.sendRequestVoteRPC(peerId, {
        term: this.currentTerm,
        candidateId: this.id,
        lastLogIndex,
        lastLogTerm
      }).then((reply) => {
        if (this.role !== "Candidate") return;

        if (reply.voteGranted) {
          votesReceived++;
          if (votesReceived > (this.peers.length + 1) / 2) {
            this.becomeLeader();
          }
        }
      });
    }
  }

  private becomeLeader() {
    this.role = "Leader";
    if (this.electionTimeout) clearTimeout(this.electionTimeout);
    console.log(`Node ${this.id} elected Leader for Term ${this.currentTerm}!`);
    this.startHeartbeats();
  }

  private startHeartbeats() {
    // Send AppendEntries heartbeats at 50ms intervals to maintain authority
    setInterval(() => {
      if (this.role !== "Leader") return;
      for (const peerId of this.peers) {
        this.sendAppendEntriesRPC(peerId);
      }
    }, 50);
  }

  // Mock network transporters for simulation environment
  private async sendRequestVoteRPC(peerId: string, args: RequestVoteArgs): Promise<RequestVoteReply> {
    return { term: this.currentTerm, voteGranted: Math.random() > 0.3 };
  }

  private async sendAppendEntriesRPC(peerId: string) {
    // Heartbeat logic
  }
}
Enter fullscreen mode Exit fullscreen mode

Split-Brain and Minority Partitions

What happens when a network partition cuts a 5-node cluster into two segments: a majority partition (3 nodes) and a minority partition (2 nodes)?

  MAJORITY PARTITION (Can commit)         MINORITY PARTITION (Cannot commit)
   [Node A] ─── [Node B (Leader 1)]         [Node D] ─── [Node E (Leader 2)]
      │                                        │
   [Node C]                                    X (Network Partition cut)
Enter fullscreen mode Exit fullscreen mode

The minority partition might elect a new leader because they can no longer hear from the original leader. However, this minority leader cannot commit any new client writes because they can never achieve the required cluster majority (3 nodes).

Once the partition heals:

  1. The minority leader receives a heartbeat containing a higher term clock from the majority leader.
  2. The minority leader immediately steps down back to a Follower.
  3. The minority nodes discard their uncommitted local log differences and synchronize their states with the majority leader's canonical log.

Raft guarantees that the cluster always converges safely back to a single source of truth.


Engineering Takeaways

  1. Raft guarantees Safety and Liveness in distributed environments with randomized timeouts.
  2. Commitment requires absolute majority consensus — protecting clusters from split-brain state divergence.
  3. Term clocks act as global logical time — higher term numbers override all stale cluster leaders.

The full article features a live 5-node Raft consensus cluster simulator — trigger network cuts to form minority and majority partitions, watch node roles transition, commit client writes, and view live log conflict resolution directly in your browser.

Read the full interactive article →


Written by Ebenezer Akinseinde — Software Developer & AI Automations Engineer.

Portfolio · GitHub

Top comments (0)