Design problems
Classic building blocksHardSDE-3 30 min

Design a distributed lock service

Asked at
AmazonGoogleMicrosoftPhonePeRazorpayUber

The interviewer asks

“Design a lock service that many services can use to make sure only one worker at a time performs a task, for example processing a given payment, running a nightly settlement job, or being the leader of a shard. It must be safe even when processes pause or the network fails.”

What a strong answer looks like

A strong answer starts by asking what the lock protects: efficiency (avoiding duplicate work, where a rare double run is acceptable) or correctness (two holders would corrupt data or move money twice). For efficiency, a Redis SET NX PX lock is fine. For correctness, build on a consensus system (etcd, ZooKeeper, or a Raft group you run) with leases, sessions and keep-alives, and, most important, fencing tokens: a number that increases with every lock grant and that the protected resource checks, so a paused old holder cannot write after its lease expired. Cover lease expiry vs process pauses (GC, VM stalls), waiting and fairness (watch the predecessor key, not the lock key, to avoid a thundering herd), lock granularity, and why Redlock is debated. Mention that many problems are better solved without a lock: idempotency keys, conditional writes or a single-partition owner.

Concepts to mention

efficiency vs correctness locksleases with keep-alivefencing tokens checked by the resourceprocess pauses and clock assumptionsconsensus (Raft/Paxos) for safe grantsRedis SET NX PX and the Redlock debatewatch predecessor to avoid thundering herdlock granularityalternatives: idempotency keys, conditional writes, partition ownership

1 Requirements

Functional

  • Acquire a named lock with a lease (TTL); return a fencing token on success
  • Release a lock (only by the current holder)
  • Renew the lease while work continues (keep-alive)
  • Blocking acquire with timeout: wait in line and be notified when the lock is free
  • Try-acquire without waiting
  • Read who holds a lock (for debugging and operations)

Non-functional

  • Safety: at most one holder per lock at any time according to the lock service, and stale holders are rejected via fencing tokens
  • Availability: survives the loss of any one node (2 of 3) or two nodes (3 of 5)
  • Acquire latency p99 under 10 ms within a region
  • Throughput: 20k lock operations/s, about 1M lock names in use
  • Liveness: a crashed holder's lock frees automatically within the lease time (e.g. 10-30 s)

Out of scope (say it out loud)

  • Cross-region global locks with single-digit ms latency (impossible due to physics; discuss)
  • Read-write locks and semaphores (mention as extensions)
  • General-purpose transactions

2 Estimates

Lock operations~20k/s writes to the consensus logAcquire + renew + release; e.g. 5k acquires/s + 5k releases/s + 10k renewals/s
Consensus cluster throughput~30-50k writes/s for a tuned etcd/Raft groupEach write = leader append + fsync on a majority; batching many entries per fsync gives tens of thousands of writes/s on NVMe
State size~500 MB1M locks × ~500 B (name, holder, lease id, token, waiters) fits easily in memory on each replica
Write latency~2-5 msClient → leader ~0.5 ms, leader → followers in other AZs ~1 ms, fsync ~0.5-1 ms on NVMe, plus queueing
Renewal load~10k/s100k active leases renewed every 10 s; renew per session, not per lock, to cut this further

3 API

EndpointWhat it does
gRPCLock.Acquire(name, ttl_ms, wait_timeout_ms) -> {token, lease_id}Grant the lock with a monotonically increasing fencing token, or wait in a queue up to the timeout.
gRPCLock.Release(name, lease_id)Release only if the caller holds it (compare lease id).
gRPCLease.KeepAlive(lease_id) streamRenews the session lease that all of a client's locks are attached to.
gRPCLock.Describe(name)Current holder, token, lease expiry and number of waiters.
Resource-sidewrite(data, fencing_token)The protected storage rejects writes whose token is lower than the highest token it has seen.

4 Data model

locksReplicated state machine (etcd / Raft log + in-memory B-tree)
nameholder_lease_idfencing_token (= Raft revision)acquired_at

All changes go through the consensus log, so every replica agrees on the holder. The log index/revision is a natural fencing token: it only increases.

lease/sessionReplicated state machine
lease_idclient_idttlexpires_at (leader-tracked)

When a lease expires, all locks attached to it are released in one log entry.

waitersReplicated state machine
keys /locks/{name}/{create_revision}

Each waiter creates its own key; the lowest revision holds the lock; each waiter watches only the key just before it (fair FIFO, no herd).

fencing high-water markIn the protected resource (e.g. Postgres row, storage service)
resource_idlast_token

UPDATE ... WHERE last_token < :token; zero rows updated means a newer holder exists.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAacquire / keepaliveacquire (wait)append entriesappend entriesfsyncexpire leasewatch event: your turnwrite + token 41Worker B (waiting)Worker A (holder)Client lib (findsleader)Lock leader (Raft)Follower AZ-bFollower AZ-cLease expiry checkerRaft log + snapshot(disk)Protected resource(DB)
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Acquire, work, release

  1. 1Worker A opens a session: Lease.Grant(ttl=15 s), and starts a keep-alive every 5 s.
  2. 2It calls Acquire('settlement-2026-10-25'); the leader appends 'create /locks/settlement-2026-10-25/<lease>' to the Raft log.
  3. 3When a majority (2 of 3) has persisted the entry, it is committed; the key's create revision (e.g. 41) is the lowest, so A holds the lock with token 41.
  4. 4A writes to the settlement table with token 41; the DB update succeeds only if 41 > last_token stored for that resource.
  5. 5A finishes and calls Release; the leader deletes A's key through the log.
  6. 6Worker B, which watches A's key, receives a delete event and now holds the lock with its own (higher) token, e.g. 57.

Holder pauses (GC) and lease expires

  1. 1Worker A enters a 40 s stop-the-world GC pause and cannot renew.
  2. 2After 15 s without keep-alive, the leader expires A's lease and deletes its lock keys in one committed entry.
  3. 3Worker B acquires the lock with token 57 and writes to the resource; the resource records last_token = 57.
  4. 4A wakes up, still believing it holds the lock, and tries to write with token 41.
  5. 5The resource rejects the write because 41 < 57. A's client library also sees the lease is gone and aborts.

7 Deep dives

Why leases alone are not enough: fencing tokens

Every distributed lock must have a lease, otherwise a crashed holder blocks everyone forever. But a lease creates a new problem: the holder cannot know for sure that its lease is still valid. A process can pause for longer than the lease (a long GC, a VM migration, a slow disk swap), or its packets can be delayed in the network. When it resumes, it continues acting as the holder while the lock service has already given the lock to someone else. Checking 'do I still hold it?' before writing does not help, because the pause can happen right after the check. The fix, described by Martin Kleppmann and used by Google Chubby (as sequencers), is a fencing token: a number that increases with each grant. The holder sends it with every write, and the protected resource remembers the highest token seen and rejects anything lower. In etcd, the key's revision serves as the token; in ZooKeeper, the zxid or the sequential node number. This moves the final safety check to the resource itself, which is the only place that can enforce it. If the resource cannot check tokens (e.g. an external payment API), use idempotency keys there instead.

Why consensus, and the Redlock debate

A single Redis node with SET lock value NX PX 30000 is fast and simple, and release must check the value with a small Lua script so you do not delete someone else's lock. But Redis replication is asynchronous: if the primary fails after granting a lock and before replicating it, the promoted replica does not know about it and grants it again. Redlock tries to fix this by acquiring the lock on a majority of 5 independent Redis nodes within a time limit. Critics (notably Kleppmann) pointed out it depends on bounded clock drift and process pauses and provides no fencing token, so it is not safe for correctness; Redis's author disagreed, and the debate is worth knowing. The safe recommendation: for efficiency locks (avoid running a cache rebuild twice), single-Redis locks are fine. For correctness locks, use a consensus-based store such as etcd, ZooKeeper or Consul, where a grant is only acknowledged after a majority has written it to disk, so leader failover never loses a grant, plus fencing tokens. The cost is a few milliseconds per operation and a cluster of 3 or 5 nodes that needs careful operation.

Waiting without a thundering herd

If 500 workers wait for the same lock and all watch the lock key, a release wakes all 500, they all try to acquire, 499 fail and go back to waiting. This herd effect wastes CPU and spikes load on the lock service at exactly the wrong moment. The ZooKeeper recipe avoids it: each waiter creates an ephemeral sequential node (lock-0000041, lock-0000042, ...). The one with the lowest number holds the lock. Every other waiter watches only the node immediately before its own. When the holder releases or dies, exactly one waiter is woken. This also gives fairness (FIFO order) and handles a waiter that dies in the middle of the line, since its successor gets an event and re-checks. etcd's concurrency package implements the same idea using create revisions. Add a wait timeout so callers do not wait forever, and keep lock scopes narrow (lock per payment ID, not one global 'payments' lock) to avoid long queues.

8 Trade-offs

Lease length

Short leases (5 s) free locks quickly after a crash but cause false expiries during GC pauses or network blips, and more renewal traffic. Long leases (60 s) are robust to pauses but block progress for up to a minute after a crash. A common choice is 10-15 s with renewal every third of the lease, plus fencing tokens to make false expiries safe.

Use a lock vs avoid locking

Locks add latency, a dependency and failure modes. Often a better design removes the need: idempotency keys with a unique constraint for payment processing, conditional updates (UPDATE ... WHERE version = v), or routing all work for a key to one Kafka partition so a single consumer owns it. Use a lock service mostly for leader election and coarse jobs, as Google's Chubby paper recommends (coarse-grained locks held for hours, not per-request locks).

Run your own vs use etcd/ZooKeeper

Writing your own Raft implementation is rarely worth it. etcd and ZooKeeper are battle-tested; DynamoDB conditional writes with a lease timestamp and a version number can also implement leases with fencing for teams already on AWS. Building a 'lock service' in an interview mostly means wrapping one of these with a clean API, sessions and fencing.

9 Bottlenecks

Consensus write throughput

All grants go through one Raft leader. Beyond ~30-50k writes/s, shard the lock namespace across several independent Raft groups (by hash of lock name), as each lock is independent. Also make renewals per session, not per lock.

Very hot single lock

If thousands of workers fight over one lock, the lock is the wrong tool. Split the work into finer-grained locks or partitions, or use a queue so work is assigned rather than contended.

10 Failure handling

Lock service leader fails

Followers elect a new leader within a few seconds (election timeout ~1-3 s). Committed grants are on a majority, so none are lost. Lease timers restart on the new leader, which by convention extends leases rather than expiring them early, so holders are not unfairly dropped. Acquires during the election wait or retry.

Client partitioned from the lock service

The client cannot renew. Its library must assume the lock is lost when the lease deadline (measured on its own monotonic clock, minus a safety margin) passes, and stop work. Fencing tokens protect the resource even if the client is slow to notice.

Majority of lock nodes down

No new grants or releases are possible (the system chooses consistency over availability). Existing holders keep working until their leases run out. Run 5 nodes across 3 AZs if you need to survive two node failures.

11 What impresses

  • Opens by asking whether the lock is for efficiency or correctness
  • Explains with a timeline how a GC pause breaks lease-only locking, then fixes it with fencing tokens
  • Knows why async Redis replication can grant a lock twice
  • Uses the etcd revision or ZooKeeper sequence number as the fencing token
  • Describes the predecessor-watch recipe for fair, herd-free waiting
  • Suggests removing the lock with idempotency or single-owner partitioning where possible

12 Mistakes

  • A lock without a TTL, so a crashed client blocks everyone forever
  • Releasing with plain DEL, which can delete another client's lock
  • Claiming a Redis lock is safe for money movement
  • Checking 'do I still hold the lock' before each write and calling it safe
  • Waking all waiters on release
  • Ignoring what happens when the lock service itself fails

13 Follow-ups

Q1How would you implement leader election for 1,000 shards using this service?
Q2The protected resource is a third-party API that cannot check tokens. What now?
Q3How would you add read-write locks or semaphores?
Q4Can you offer a lock across Mumbai and Singapore regions? What latency would it have?
Q5How do you test lock safety (e.g. Jepsen-style fault injection)?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free