Build Raft — consensus you can defend (12 scenes)
Scene 03 · Leader election — vote, majority, restriction
Election timeout → candidate → RequestVote → majority grants → leader. Voters grant only if the candidate's log is at-least-as-up-to-date — a placeholder predicate refined precisely in scene 6.
Previously

Scene 2 gave us a term — a monotonically-increasing integer that segments time and makes any server reset to follower the moment it sees a number bigger than its own. The obvious next question: which server is the leader of the current term? Each term needs at most one, and the cluster has to pick one without a coordinator. That picking process is leader election.

Scene 03
Leader election — vote, majority, restriction
Diagram
Five servers (S1..S5) drawn as cells in a row. Each cell shows the server's id, role badge (FOLLOWER / CANDIDATE / LEADER — same three roles from scene 2), currentTerm (the latest term it has seen), votedFor (who, if anyone, it has voted for in the current term), and a horizontal log strip whose entries are colored by the term that created them. Arrows between cells render vote-request and heartbeat messages — amber for vote requests, blue (dashed when carrying no new entries) for heartbeats; a check mark means the vote was granted, an X means refused.
S1FOLLOWERT:1vote:—1t1commitS2FOLLOWERT:1vote:—1t1commitS3FOLLOWERT:1vote:—1t1commitS4FOLLOWERT:1vote:—1t1commitS5FOLLOWERT:1vote:—1t1committerm colors: 1 blue · 2 teal · 3 amber · 4 violet · 5 roseAppendEntriesheartbeatRequestVoteInstallSnapshotcommitIndexFive followers, all at term 1. S1 has not heard from a leader in a while — its election timeout (the random wait that says 'no le…
election timeout fires →
Now that we have a term that monotonically segments time, the obvious question is: which server is the leader of the current term? A follower that hears nothing from a leader for a while gives up waiting and tries to become leader itself. The 'while' is a random wait — somewhere between 150 and 300 ms in the standard configuration — and we call it the **election timeout**: the randomized interval after which a follower that has received no contact from a leader transitions to **candidate** and begins an election. Watch: S1's timeout fires first. It bumps its **currentTerm** from 1 to 2, marks itself with **votedFor=S1** (the persisted record of who, if anyone, this server has voted for in the current term — at most one per term), and sends every peer a 'will you vote for me?' message. The systems term for that message is a **RequestVote RPC** — short for Remote Procedure Call, which just means 'a function call sent over the network'; the call carries the candidate's term, its id, and the position of its last log entry so the voter can decide whether to grant. Four yes-replies come back; combined with its own self-vote that is 5 out of 5, comfortably above the **majority quorum** (more than half of the cluster — for 5 servers, that's 3). S1 becomes leader of term 2 and immediately sends an empty append (a **heartbeat**) to tell every follower it is in charge.
Implementation
Candidate.beginElection
election timeout fires; candidate broadcasts RequestVote
1on election_timeout: # randomized 150–300 ms
2 role = candidate
3 currentTerm += 1
4 votedFor = self
5 persist(currentTerm, votedFor)
6 votes = { self }
7 reset election_timeout
8 for peer in cluster_minus_self:
9 send RequestVote {
10 term: currentTerm,
11 candidateId: self,
12 lastLogIndex: log.lastIndex,
13 lastLogTerm: log.lastTerm,
14 } -> peer
Voter.onRequestVote(req)
grant iff free vote AND candidate's log up-to-date
1on receive RequestVote(req):
2 # (universal step-down already handled at handler top — see scene 2)
3 up_to_date = isAtLeastAsUpToDate(
4 req.lastLogIndex, req.lastLogTerm, log,
5 )
6 # PLACEHOLDER — precise predicate is sharpened in scene 6 (§5.4.1).
7 if (votedFor == null OR votedFor == req.candidateId) AND up_to_date:
8 votedFor = req.candidateId
9 persist(currentTerm, votedFor)
10 reply { term: currentTerm, voteGranted: true }
11 else:
12 reply { term: currentTerm, voteGranted: false }
Candidate.onRequestVoteReply
majority of grants → leader; broadcast heartbeat
1on receive RequestVoteReply(reply, from peer):
2 if reply.term > currentTerm: # (step-down at handler top)
3 return
4 if reply.voteGranted:
5 votes.add(peer)
6 if |votes| >= majority(cluster.size): # ⌊N/2⌋ + 1
7 role = leader
8 initialize nextIndex[p] = log.lastIndex + 1 for p in cluster
9 initialize matchIndex[p] = 0 for p in cluster
10 broadcast empty AppendEntries (heartbeat) # claim leadership