Coordination Patterns

📖 12 min read

Some work only goes right when exactly one node does it: one scheduler assigning jobs, one writer updating a record, one controller deciding partition ownership. On a single machine a mutex settles that. Across a network, nodes crash without saying so, messages arrive late, clocks disagree, and a process can pause for long enough that the world moves on without it. Coordination patterns are the ways systems still get to “exactly one” under those conditions, and each one depends on the one below it.

  • Leader election picks a single node to act for the group.
  • Distributed locks grant one node exclusive access to one resource for a while.
  • Consensus is what makes both of those safe, by getting a majority of nodes to agree on a value even when some of them fail.

Leader Election

One node in a group is chosen to coordinate. It assigns work, makes decisions, or acts as the single writer for some shared state. When it fails, the remaining nodes choose another.

State machine

Leader Failover

The leader fails, followers notice, and a new leader is chosen.

Leader failover in a three-node group Three states of a three-node group. First, node 1 is the leader and nodes 2 and 3 are followers. Second, node 1 fails and the followers detect the failure through a heartbeat timeout. Third, a new election makes node 2 the leader, node 3 stays a follower, and node 1 remains down. 1. STEADY STATE Node 1: leader Node 2: follower Node 3: follower 2. LEADER FAILS Node 1: failed ✗ Node 2: follower Node 3: follower Followers detect the failure through a heartbeat timeout. 3. NEW ELECTION Node 1: down ✗ Node 2: leader Node 3: follower A new leader is chosen.

Use when:

  • A task such as job scheduling or partition assignment must have one coordinator
  • Some work must happen once, and running it on every node would duplicate it
  • Shared state needs a single writer

Election mechanisms:

Mechanism How it works Safe under network partitions?
Bully algorithm The highest-ID reachable node claims leadership No. Two sides of a partition can each elect their own leader
Raft leader election Nodes vote in numbered terms, and a candidate needs a majority Yes. Only one side of a partition can hold a majority
Coordination service Nodes race to create a key or ephemeral node in ZooKeeper, etcd, or Consul, and the winner leads Yes, because the service itself runs consensus underneath

Using a coordination service is the common choice in application code, since it hands the hard part to a system built for it. The ZooKeeper form looks like this.

1. Every node tries to create the ephemeral node /election/leader
   Node 1: CREATE /election/leader → SUCCESS (becomes leader)
   Node 2: CREATE /election/leader → FAIL (node exists)
   Node 3: CREATE /election/leader → FAIL (node exists)

2. Followers watch /election/leader for deletion

3. Leader's session expires → ZooKeeper deletes the ephemeral node

4. Followers are notified and race to create it again
   Node 2: CREATE /election/leader → SUCCESS (new leader)
   Node 3: CREATE /election/leader → FAIL

Systems that need election internally increasingly embed Raft instead of depending on an external service. Kafka is the prominent example, having replaced ZooKeeper with its own Raft-based KRaft controller and removed ZooKeeper support entirely in Kafka 4.0.

Leases Bound How Long a Leader Can Be Wrong

A node that has been voted out doesn’t necessarily know it. If it paused or lost connectivity, it may resume still believing it leads, while a new leader is already acting. That is split-brain. A lease limits it: leadership is granted for a fixed period that the leader must keep renewing, and a new leader is only chosen after the old lease has expired.

Chart

Leader Lease

A crashed leader's lease runs out before anyone else leads.

Leader lease timeline A timeline with two rows. Node 1 acquires a lease at T=0 that expires at T=10 and renews it at T=5 so it expires at T=15. Node 1 crashes at T=8 and stops renewing, but its lease is still held until it expires at T=15. Node 2 is a follower until T=16, when it acquires a new lease and becomes leader. From T=8 to T=15 there is no leader, which is safer than two. T=8 to T=15: no leader, which is safer than two Node 1 acquire: expires T=10 renew: expires T=15 leader crashed, lease still held Node 2 follower leader new lease time T=0 T=5 T=8 crash, renewals stop T=10 T=15 lease expires T=16

A lease only works if the old leader checks it before acting, and even then it relies on clocks and on the leader not pausing between the check and the action. A garbage collection pause that starts right after a successful check leaves the old leader acting on a lease that expired while it was paused. Leases shrink the window, but they don’t close it. Fencing tokens, covered under distributed locks below, close it.

Trade-offs: Every failover leaves a gap with no leader, at least as long as the heartbeat timeout plus the lease. Short timeouts shorten the gap but trigger elections on ordinary network slowness, and each unnecessary election is its own brief outage. Concentrating work on one node also caps throughput at what that node can do.


Distributed Lock

A lock that holds across a cluster, so that only one node at a time works on a given resource. Unlike a local mutex, it has to survive the holder crashing, the network dropping, and the holder pausing without knowing it paused.

Flow

Lost Update Without a Lock

Two read-modify-writes race, and a distributed lock serializes them.

A lost update without a lock, and the same updates under a distributed lock Two panels, time running downward. Without a lock, node A and node B both read 10. A adds 5 and writes 15, B adds 3 and writes 13, and the final value is 13 because A's update was lost. With a distributed lock, A acquires the lock while B's acquire is blocked. A reads 10, adds 5, writes 15, and releases. B then acquires the lock, reads 15, adds 3, and writes 18, so the final value is 18. time WITHOUT A LOCK Node A Node B read 10 add 5 write 15 read 10 add 3 write 13 Final value: 13 (lost update) WITH A DISTRIBUTED LOCK Node A Node B acquire read 10 add 5 write 15 release acquire blocked, waiting acquire read 15 add 3 write 18 Final value: 18

Use when:

  • Several nodes may act on the same resource at the same time
  • An operation should not run twice concurrently, such as sending a batch of emails
  • Shared state is updated through a read-modify-write that the storage can’t make atomic itself

Every Lock Needs an Expiry

A lock with no expiry is held forever by a node that crashed while holding it. So distributed locks carry a time-to-live, and a single Redis instance shows the basic mechanics.

Acquire:
  SET lock:order-123 "node-A-7f3c" NX PX 30000
    NX       = only set if the key doesn't exist
    PX 30000 = expire after 30 seconds
  OK  → lock acquired
  nil → someone else holds it; wait or retry

Release, atomically, only if we still own it:
  if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
  else
    return 0
  end

The release checks ownership because the lock may already have expired and been taken by someone else. Deleting without checking would release another node’s lock.

Expiry Breaks Mutual Exclusion, and Fencing Restores It

The expiry that saves you from a crashed holder creates a new problem with a slow one. The holder can stall for longer than the TTL, whether from a garbage collection pause, a swapped-out process, or a network delay, then wake and carry on writing, unaware its lock expired and a second node now holds it.

Flow

Lock Expiry Under a Paused Holder

A paused holder's lock expires, and it writes anyway.

A lock expires while its holder is paused A timeline with two rows. Node A acquires the lock, then enters a long garbage collection pause. During the pause the lock expires, and node B acquires it and writes successfully. Node A then wakes and writes, unaware that its lock expired, so its write is stale. Node A's lock expires Node A acquire lock long GC pause write ✗ stale Node B acquire lock write ✓ time

No check inside Node A can prevent this, because the pause can happen after the check. The fix has to live at the resource being protected. A fencing token is a number that increases every time the lock is granted. The holder sends its token with every write, and the storage rejects any write carrying a token lower than the highest it has already seen.

Flow

Fencing Token

The storage rejects a write whose token is older than one it has seen.

Fencing tokens reject a stale write A timeline with three rows. Node A acquires the lock with token 33, then pauses for a long time. Node B acquires the lock with token 34 and writes with token 34, which the storage accepts, and from then on the storage requires a token of at least 34. Node A wakes and writes with token 33, and the storage rejects it. Node A lock, token 33 long pause write(token 33) Node B lock, token 34 write(token 34) Storage accepted REJECTED: 33 < 34 From here the storage requires a token of at least 34. time

That requires two things: a lock service that issues monotonically increasing tokens, and a resource that checks them. ZooKeeper’s znode version or transaction id and etcd’s revision numbers both serve as tokens. A database can enforce the check with a conditional update on a stored token column.

Efficiency Locks and Correctness Locks

How much of this applies depends on what happens if two nodes do hold the lock at once. Martin Kleppmann’s analysis of distributed locking draws the line in two places.

Purpose What a lock failure costs What’s adequate
Efficiency: avoid doing the same work twice Some wasted work, or a duplicate notification A single Redis instance with a TTL
Correctness: stop concurrent writers corrupting state Lost updates, or permanently inconsistent data A consensus-backed lock service issuing fencing tokens, with the resource enforcing them

Redlock, Redis’s multi-instance algorithm, takes a lock on a majority of independent Redis nodes. It survives the loss of individual Redis nodes better than a single instance, but it doesn’t make the lock suitable for correctness. It generates no fencing tokens, and its safety depends on bounded network delay, bounded process pauses, and bounded clock drift, all of which real systems violate. It sits in the efficiency row, with more operational cost than a single instance for little gain there.

Common implementations:

  • Redis, single instance or Redlock, for efficiency locks
  • ZooKeeper, using ephemeral sequential nodes, which also gives fair queuing among waiters
  • etcd, using leases and its lock API
  • Consul, using sessions

Look for a Way Not to Lock

A distributed lock is often standing in for something the storage can already do. A unique constraint prevents duplicate inserts, an optimistic concurrency check on a version column prevents lost updates, and a queue with competing consumers hands each message to one worker. Each of these puts the guarantee where the data is, which is where fencing would have had to put it anyway.

Trade-offs: Every acquisition is a network round trip to the lock service, so a lock on a hot path becomes a throughput limit. The lock service is a dependency whose outage blocks every holder. And a lock that is correct needs cooperation from the resource it protects, which makes it a design change to that resource, not a wrapper around existing code.


Consensus

Consensus gets a group of nodes to agree on a value, such as which node leads, whether a write committed, or the order of entries in a log, in a way that stays agreed even when some nodes fail. It is what leader election and correctness locks rest on, and application code rarely implements it directly. It uses a coordination service or a database that has consensus built in.

The core rule is a majority quorum. A cluster of 2f+1 nodes keeps working with up to f of them failed, because any two majorities overlap in at least one node, and that node carries forward what was decided. Three nodes tolerate one failure, and five tolerate two.

C4 · Dynamic

Consensus Write Path

A write commits once a majority of the cluster acknowledges it.

A write replicated through a three-node consensus group A client sends a write request to the leader. The leader appends the entry to its log and replicates it to follower 1 and follower 2. When a majority, two of three nodes, has acknowledged, the entry is committed and the leader replies to the client. Client CONSENSUS GROUP, 3 NODES Leader appends entry to its log Follower 1 Follower 2 1. write request 4. reply 2. replicate 3. ack 3. ack Committed once a majority (2 of 3) has acknowledged. request or replication acknowledgement

Use when:

  • A decision must hold even if some of the nodes that made it are lost
  • Replicated state has to be identical across nodes, in the same order
  • Building or operating the coordination layer that other services depend on
  Paxos Raft
Origin Leslie Lamport, “The Part-Time Parliament”, circulated from 1989 and published in 1998 Diego Ongaro and John Ousterhout, “In Search of an Understandable Consensus Algorithm”, 2014
Design goal Correctness, stated minimally Understandability, with leader election, log replication, and safety as separate parts
Reputation Notoriously hard to understand and to turn into a working system Easier to reason about, which is much of why it spread
Found in Google Chubby, and Cassandra’s lightweight transactions etcd, Consul, CockroachDB, and Kafka’s KRaft controller

Both assume nodes fail by crashing or going silent, not by lying. Tolerating nodes that behave arbitrarily or maliciously is the Byzantine fault tolerance problem, which algorithms such as PBFT address at a higher price: 3f+1 nodes to tolerate f faulty ones. That cost is why BFT shows up in blockchains and systems spanning mutually distrustful parties, and rarely inside one organization’s infrastructure.

Trade-offs: A consensus group stops accepting writes when it loses its majority, so during a partition the minority side is unavailable by design, choosing consistency over availability. Every write waits for a majority to acknowledge it, so latency is set by the slower members of the quorum, and spreading nodes across regions for resilience makes every write pay cross-region latency. Adding nodes improves fault tolerance but not write throughput, which is why consensus clusters stay small, typically three or five nodes.


Quick Reference

Pattern What it guarantees Depends on Main cost
Leader election At most one leader at a time, if backed by consensus and leases Consensus, and fencing for writes the old leader might still make A leaderless gap on every failover
Distributed lock One holder at a time for efficiency, and for correctness only with fencing A lock service, and a resource that enforces fencing tokens A round trip per acquisition, and a dependency every holder shares
Consensus Agreement that survives the failure of a minority of nodes A majority of nodes being reachable Unavailability without a majority, and quorum latency on every write

Found this guide helpful? Share it with your team:

Share on LinkedIn