Distributed Lock Interview Questions | JiQuest

add

#

Distributed Lock

System Design Interview Guide · Post 2

Design a Distributed Lock Manager (Redlock / ZooKeeper-style)

A mutual-exclusion service that lets independent processes on independent hosts safely agree "only one of us touches this resource right now" — even when nodes crash, clocks drift, and garbage collectors freeze the world.

3 of 5Redlock quorum
<10msP99 acquire latency
10sdefault TTL / lease
Client N1 N2 N3 N4 N5 quorum 3/5 → token 34 majority reached before TTL

1. Requirements & scope

Clarify these out loud before drawing boxes — a lock manager's entire value proposition rests on the guarantees it makes under failure, not on its happy path.

Functional

Mutual exclusionOnly one process across any number of hosts may hold the lock for a given resource key at a time.
Automatic lease expiryLocks carry a TTL so a crashed or partitioned holder can never deadlock every other client forever.
Fencing tokensEvery successful acquisition returns a monotonically increasing token the protected resource can use to reject stale writes.
Leader-election primitiveThe same acquire/renew/release API doubles as the building block for electing a single active leader in a cluster.

Non-functional

Fault tolerantSurvives the loss of a minority of lock nodes without losing availability or safety.
Low latencyAcquisition should complete in single-digit milliseconds so it doesn't dominate the critical section it protects.
Safety under skewNever grants two valid locks for the same resource simultaneously, even across GC pauses, process suspends, and clock drift.
Horizontally scalableMillions of independent resource keys, each locked independently, with no coordination between unrelated keys.
Explicitly out of scope General-purpose distributed transactions, multi-key atomic commit (2PC/Paxos-backed transactions), byte-range file locking, and application-level deadlock detection between multiple locks held by the same client. This design is a single-resource, single-holder mutual-exclusion primitive with a lease.

2. Capacity estimation

Lock services are read/write-light per operation but latency-critical, and the sizing question that actually matters is "how many independent lock nodes, and what quorum" — not raw throughput.

DimensionEstimateWhy it matters
Acquisitions / sec (peak)~50,000 ops/sec across all resource keysEach op is a handful of SETNX-style calls fanned out to N nodes — cheap, but fan-out multiplies node-side QPS by N.
Lock nodes (Redlock)5 independent masters, majority = 3Odd N tolerates ⌈N/2⌉-1 node failures while a majority can still agree; 5 is the common sweet spot between fault tolerance and fan-out cost.
Lease / TTL duration10s default, configurable 1s–300sMust comfortably exceed expected clock drift + network round trip + client pause time, or a live holder can be declared dead too early.
Concurrently held locks~200,000 active sessionsDrives memory footprint on each lock node (a lock record is tiny: key, token, expiry) and heartbeat fan-in rate.
Latency budget / acquisition<10ms P99Must stay well under the TTL and under the caller's own SLA, since the caller blocks (or backs off) waiting on the result.
Sizing takeaway 50k acquisitions/sec × 5-node fan-out ≈ 250k small ops/sec on the lock tier — trivially handled by in-memory nodes, so the real design constraint is quorum safety and TTL correctness, not raw capacity.

3. High-level design

Five moving parts: the caller, a client-side library that owns quorum logic, N independent lock nodes, a fencing-token issuer, and a background renewal loop.

Client app Lock client library quorum, retry, backoff clock-drift guard Lock node N1 Lock node N2 Lock node N3 Lock node N4 Lock node N5 Fencing token issuer monotonic counter / resource Heartbeat / lease renewal background thread, every TTL/3 writes SET key token NX PX ttl
voted / majority nodes unreached or slow node fencing-token path renewal / heartbeat path

What each box owns

Lock client library

Owns the actual algorithm: it fans an acquire attempt out to all N nodes in parallel, waits for acknowledgements within a time budget well under the TTL, and declares success only if a strict majority answered "granted" before that budget expires. It also owns exponential backoff on contention and a random jitter to avoid thundering-herd retries.

Lock nodes (Redis instances or ZooKeeper/etcd servers)

Each node is independent and holds no replica of any other node's state (for Redlock) or is one voting member of a single consensus ensemble (for ZooKeeper/etcd). A node grants a lock via an atomic conditional write (SET key token NX PX ttl) and nothing more — it does not need to know about the other nodes.

Fencing-token issuer

A strictly increasing counter, one per resource key, incremented on every successful acquisition. The token rides along with every write the lock holder makes to the protected resource, so the resource itself can reject any write carrying a token lower than the highest it has already seen.

Heartbeat / lease-renewal loop

A background thread on the client side that extends the lease (re-issues the conditional SET with a fresh TTL, only if the token still matches) roughly every TTL/3. If the holder process is paused or dies, renewal simply stops and the lease is left to expire naturally — no separate liveness protocol is needed.

4. Deeper architecture & failure domains

The detail that actually decides safety: are the N lock nodes truly independent (Redlock) or are they one replicated state machine (ZooKeeper/etcd)? Both are legitimate, but they fail differently.

Redlock: 5 independent masters, no replication AZ-a Redis N1 AZ-b Redis N2 AZ-c Redis N3 AZ-d Redis N4 AZ-e Redis N5 ⚠ no cross-node link: each master is fully independent, majority = safety ZooKeeper / etcd: one consensus ensemble Leader (ZAB / Raft) Follower F2 ephemeral znode / lease = the lock; session loss auto-deletes it Client library: retry + backoff + jitter, session/heartbeat manager talks to either backend behind the same acquire/renew/release API
independent Redlock master idle / not needed for quorum ZooKeeper/etcd consensus member shared client-side plumbing
DecisionChoice & when it applies
Redlock vs ZooKeeper/etcdRedlock (5 independent Redis nodes): lowest latency, no consensus round trip, good when the cost of a rare double-grant is tolerable (e.g. cache stampede protection). ZooKeeper/etcd: strict linearizable consensus with ephemeral/lease-backed nodes and watches, correct choice when the protected action is destructive or irreversible (e.g. leader election, schema migration).
TTL lengthShort TTL (1–5s) recovers fast from a dead holder but risks the lease expiring under a real holder's transient pause; long TTL (30s+) is safer against false expiry but slows recovery and lengthens contention. Pick TTL ≫ (max GC pause + max clock drift + network RTT).
Fencing token implementationMonotonic per-resource counter, persisted alongside the lock record so a node restart doesn't reset it; the protected resource (database row, S3 object, etc.) stores "highest token seen" and rejects anything lower — this is enforced outside the lock service entirely.
Clock-dependency riskRedlock's safety proof assumes bounded clock drift and bounded pauses, which async systems cannot fully guarantee; mitigate with NTP-disciplined clocks, conservative TTLs, and treating fencing tokens (not the lock itself) as the actual safety mechanism.

5. Sequence walkthroughs

Two flows matter more than any other diagram in this design: the quorum acquisition happy path, and the fencing-token check that saves you when the happy path assumptions break.

5.1 Lock acquisition with lease / TTL

Client Client lib Lock nodes (5) Fencing issuer 1. acquire("res-42") 2. SET res-42 token NX PX 10s (x5, parallel) 3. 3 of 5 reply OK within budget 4. next token for res-42 → 34 5. lock granted, token=34, ttl=10s 6. background renew every ~3.3s (SET only if token still 34) 7. release: DEL if token == 34 (Lua/CAS) 8. resource free for next acquirer

The client library never waits for all 5 nodes — only a majority, and only within a time budget short enough that the sum of "time spent acquiring" plus "time already elapsed on the TTL" leaves a safe margin before expiry. If the budget is exceeded, the client releases whatever partial grants it got and retries after a random backoff, so contention resolves without a thundering herd.

5.2 Fencing token preventing a stale-lock write

Client A Lock service Client B Storage 1. A acquires lock, token=33 2. A hits a long stop-the-world GC pause 3. lease TTL expires while A is paused 4. B acquires the same lock, token=34 5. B writes with token=34, storage records "seen: 34" 6. A wakes up, still believes it holds the lock 7. A writes with stale token=33 → storage rejects (33 < 34) 8. Safety preserved despite the false lock loss

This is the mechanism that actually makes a distributed lock safe: the lock service can never fully rule out a false expiry under async networks and pausable processes, so safety is pushed one layer down to the storage system, which only needs to remember one integer per resource. Client A's write is not blocked by the lock — it is rejected by storage because it arrives with a token the storage layer has already superseded.

6. Data model (the lock service's own control plane)

This ER diagram is not the application's data — it models what the lock service itself persists (or holds in memory) to track who owns what, for how long, and with which fencing token.

lock_records PK resource_key holder_token (FK) holder_session_id (FK) expires_at acquired_at fencing_tokens PK resource_key last_token (monotonic) updated_at sessions PK session_id client_id last_heartbeat_at ttl_ms resource (external) stores "highest token seen" rejects writes below it 1:1 1:1 issues

Key modeling decisions

Fencing tokens must be monotonically increasing per resourceA restart or failover must never reissue a token already handed out, or two clients could legitimately hold "the same" token — so the counter is persisted, not reconstructed from wall-clock time.
Sessions live separately from lock_recordsA single client session can hold zero, one, or (in the leader-election case) be watched by many resources; decoupling heartbeat liveness from the lock itself lets one renewal loop cover multiple locks.
resource_key needs a unique indexThe entire safety guarantee reduces to "at most one row per resource_key at a time" — this is the conditional write (NX) enforced at the storage layer, not application logic.
expires_at is a plain timestamp, not a TTL countdownStoring an absolute expiry (not a remaining duration) means any node can independently decide "is this lock still valid" without needing a synchronized countdown timer.
Storage backendTrade-off
In-memory Redis + TTL (Redlock)Fastest, simplest; safety depends on the 5-node quorum assumption and bounded clock drift, and is the design Kleppmann's critique targets directly.
ZooKeeper ephemeral znodesLinearizable via ZAB consensus; a znode is auto-deleted when the owning session's heartbeat stops, giving lock release "for free" on crash, at the cost of a consensus round trip per write.
etcd leasesRaft-backed, same linearizability class as ZooKeeper; leases are a first-class primitive (attach a lease to a key, key vanishes when the lease expires), often simpler operationally than ZK.

7. Deep-dive Q&A

"Isn't Redlock provably unsafe? Didn't Kleppmann write a takedown of it?"

Yes, and it's worth knowing the argument, not just the headline. Martin Kleppmann's 2016 critique showed that Redlock's safety proof implicitly assumes bounded process pauses and bounded clock drift — assumptions that async, GC-managed, virtualized systems cannot fully guarantee. His worked example: Client A acquires the lock, then a long GC pause (or VM migration, or swap) freezes it well past the TTL; the lock expires and Client B legitimately acquires it and writes; A wakes up and, still believing it holds the lock, writes too — two "lock holders" both wrote.

Salvatore Sanfilippo (Redis's author) responded that Redlock was never claiming to solve this class of problem alone — the fix he and Kleppmann both converge on is fencing tokens: the lock grants exclusivity in the common case, but the actual safety guarantee against a false-expiry race is enforced by the protected resource checking a monotonic token, not by the lock service's timing assumptions.

"Why are fencing tokens necessary rather than a nice-to-have?"

Because a lock, on its own, is a piece of advice: it tells a client it may proceed, but nothing physically stops a client that has been descheduled and later resumes from also proceeding, unaware time has passed. Without a token, the only way to try to fix this is by shrinking TTLs and hoping pauses never exceed them — which is a bet, not a guarantee. A fencing token converts "please don't write" into "your write will be mechanically rejected if you're late," because the check happens at the point of actual effect (the database write, the file append, the API call), where a simple integer comparison is trivial to enforce correctly.

"Walk me through how a stop-the-world GC pause is actually handled."

The lock service does nothing special for it — that's the point. The lease simply expires on schedule because the paused client's renewal heartbeat silently stops arriving; there is no separate "detect the pause" step. What handles the pause safely is downstream: when the paused client resumes and tries to act as if it still holds the lock, its fencing token is now stale, and the resource it's writing to rejects the write. The lock's TTL bounds how long an orphaned lock can block others; the fencing token bounds what damage a resurrected holder can do.

"How would you build leader election on top of this?"

Every candidate node attempts to acquire a well-known lock key (e.g. leader/order-service) at startup and on any failure. The winner renews it continuously as its heartbeat, and treats "renewal failed / lease lost" as an immediate signal to step down and stop performing leader-only actions — before a competing node can even try to acquire. All leader-only writes must carry the fencing token from that acquisition, so that if two nodes ever briefly believe they're leader (the same GC-pause scenario above), only the writes from the higher token succeed.

// storage-side fencing check, language agnostic
function applyWrite(resourceKey, token, payload) {
  const lastSeen = getLastToken(resourceKey);   // persisted, not from the lock service
  if (token <= lastSeen) {
    throw new StaleTokenError(`rejected: token ${token} <= last seen ${lastSeen}`);
  }
  setLastToken(resourceKey, token);
  persist(resourceKey, payload);
}

"What happens under a network partition or split-brain?"

For Redlock, if a partition splits the 5 nodes such that no side can see a majority, no side can acquire — the system fails toward unavailability, not toward two holders, which is the correct trade-off for a mutual-exclusion primitive. If the partition heals right at a TTL boundary, it is possible (per Kleppmann's critique) for both sides to have briefly believed they held the lock; fencing tokens are what make that survivable. For ZooKeeper/etcd, the consensus layer itself refuses to make progress without a quorum, so the same "unavailable over unsafe" property holds, plus you get a linearizable, single history of who held the lock and when — useful when you need an audit trail, not just mutual exclusion.

8. Summary

A distributed lock manager is deceptively small in surface area — acquire, renew, release — but the entire interview is really about what backs those three calls: a quorum of independent nodes or a consensus ensemble, a TTL that bounds the blast radius of a dead holder, and a fencing token that is the actual, mechanically-enforced safety net once you accept that no timing assumption is ever airtight.

RedlockZooKeeper & ZABetcd & RaftFencing tokensLeader electionQuorum consensus
Interview tip The strongest signal you can give in this interview is saying, unprompted, that a lock by itself does not guarantee mutual exclusion under async networks and GC pauses — only a lock combined with a fencing token enforced at the resource does. Interviewers who ask about distributed locks are almost always probing for exactly this distinction.
No comments
Leave a Comment