Skip to content
System design course
Ch.4 · Designing real systems·concept ·8 min read

Designing a distributed lock service

Build a service that lets many nodes coordinate exclusive access to a resource — the subtle correctness pitfalls of locks across an unreliable network.


The problem

Design a distributed lock service: a way for processes on different machines to agree that only one of them holds a lock on a resource at a time (leader election, exclusive access to a file, preventing duplicate work). On a single machine a mutex does this; across a network, locks are surprisingly treacherous — this problem is really about understanding why.

Step 1 — Requirements

Functional: acquire(resource, owner, ttl), release(resource, owner), optionally renew. Only one owner holds a given lock at a time.

Non-functional: correctness/safety (never two holders — the whole point), liveness (a crashed holder’s lock is eventually released), availability, and low latency. Safety usually beats availability here (CP).

Step 2 — Why network locks are hard

Three failure modes make naive locks unsafe:

  • The holder crashes while holding the lock → without a timeout, the lock is stuck forever. → Locks need a TTL/lease.
  • The TTL expires while the holder is still working (a long GC pause or slow network) → the lock is granted to someone else, and now two processes act on the resource. → Need fencing (below).
  • The lock service itself partitions → must not grant the same lock on both sides. → Need consensus/quorum.

Acknowledging these up front is the senior signal — most candidates draw a naive lock and miss them.

Step 3 — A simple lease lock (and its flaw)

The common starting point: a lock in Redis with a TTL.

SET lock:resource <owner> NX PX 30000     # set if absent, expire in 30s
release: delete only if value == my owner  (atomic via Lua)

NX gives mutual exclusion; PX (TTL) handles a crashed holder. But if the holder pauses past the TTL, a second holder acquires it — two holders. This lease lock is fine for efficiency (“usually only one runs”) but not safe for correctness without fencing.

Step 4 — Fencing tokens (making it safe)

Each lock grant returns a monotonically increasing fencing token. The protected resource (DB, storage) rejects any write with a token lower than the highest it has seen. So even if an old, paused holder thinks it still holds the lock, its writes are rejected because a newer holder has a higher token. This converts a lease lock into a safe one — name it explicitly.

Step 5 — Making the lock service itself reliable

A single lock node is a SPOF. Production lock services run on a consensus cluster:

  • ZooKeeper — ephemeral sequential znodes: each client creates a node; the lowest sequence holds the lock; ephemeral means a crashed client’s node vanishes (auto-release); others watch their predecessor (no busy-wait).
  • etcd — leases + compare-and-swap on a Raft-replicated key.
  • Redlock — a multi-node Redis algorithm (acquire on a majority); controversial for strict correctness — prefer consensus-based locks when safety matters.

These use a quorum so a partition can’t grant the lock twice (CP).

Trade-offs to raise

  • Safety vs availability — a strict lock is CP; during a partition it may refuse to grant rather than risk two holders.
  • Lease length — too short → spurious expiry under load; too long → slow recovery from a crashed holder.
  • Efficiency locks vs correctness locks — a Redis lease lock is fine to avoid duplicate work; only correctness-critical resources need fencing + consensus.

The interview cue

“A lease with TTL handles crashes; but a paused holder can outlive its lease, so for correctness I add fencing tokens that the resource enforces. The lock service runs on a consensus cluster (ZooKeeper ephemeral znodes / etcd) so a partition can’t double-grant — it’s CP.” Surfacing the TTL-expiry-while-working hazard and fencing is exactly what this problem tests.