System design interview

Design a distributed lock service

Consensus replication, leases, session heartbeats and fencing tokens.

At a glance

Level
Advanced
Topics
Consistency
Main parts
Guarded resource · Lock leader · Replicas · Raft log · Lock table
What you'll learn
Practise this problem
Overview
ClientGuardedresourceLock leaderReplicasRaft logLock tableClientGuarded resourceLock leaderReplicasRaft logdurable, orderedLock table

Press Play to follow a request through the design, or step through it.

  • Client and edge
  • Service
  • Data store
  1. A client asks for the lock named nightly-report, on a session it opened earlier.
  2. The leader appends an acquire entry to its own log. It is only a proposal until a majority has it.
  3. It sends the entry to the replicas. Each one stores it on disk before it answers.
  4. Two replicas answer. With the leader, three of five servers hold the entry, so it is committed.
  5. The leader applies the committed entry: the lock is now owned by this session, with the next token, 8,412.
  6. The lock table confirms the new owner and token.
  7. The client is told it holds the lock, with fencing token 8,412.
  • Client
  • Guarded resource: rejects old tokens
  • Lock leader: one of five servers
  • Replicas: four other servers
  • Raft log: durable, ordered
  • Lock table: owner, token, expiry
On this page

In 2 minutes

Key ideas
  • The numbers. The data is small, so the cost is not storage. It is the agreement on every change and the heartbeat load.
  • Where lock state lives. A consensus-replicated group of 5.
  • Lease granularity. One lease per client session.
  • Where safety is enforced. Fencing tokens checked by the resource.
  • Cluster size. 5 servers.
Read the 2-minute answer

I would run the lock service as five servers that agree on every change using a Raft-style log. One server leads. Acquiring a lock appends an entry, which counts as committed once three of the five have stored it, about 3 ms in a region. The entry is then applied to a lock table in the same order on every server, so no failover can forget a lock or grant it twice, and two servers can fail without loss.

Clients open a session with a 10-second lease and send a heartbeat every 3 seconds. All of a client's locks hang off the session, so the load is about 33,000 heartbeats a second rather than 333,000. The leader times sessions on its own monotonic clock, and a new leader gives every session a fresh lease, which can only delay a release, never hasten it. Leases alone are not enough, because a client can pause after checking its lease.

So each grant carries a fencing token that grows with every grant, and the client sends it with every write. The protected resource keeps the highest token it has seen and rejects lower ones. The trade-off is that the resource must support a token check, in exchange for safety that does not depend on timing.

Step 1 of 6: Clarify the scope

Questions to ask first

Ask these before you draw anything. Guess why each answer changes the design, then open it to check.

Is the lock for efficiency or for correctness?
Why it matters: To avoid duplicate work, a rare double hold is only a waste. To protect data, it is a corruption, and we need fencing tokens.
Do we need shared read locks as well as exclusive locks?
Why it matters: Shared locks add a count of readers and rules for letting a writer in, so we start with exclusive locks only.
How long are locks usually held, and how fast must a dead holder be replaced?
Why it matters: Hold time and failover time set the lease length. A short lease recovers fast and risks false expiry.
How many locks and clients are there, and how many operations a second?
Why it matters: A lock table of a million entries fits in memory. The heartbeat rate is what drives the load.
Does the resource being protected let us add a check, such as a conditional write?
Why it matters: Fencing only works if the resource can reject an old token. If it cannot, the lock can only be a best effort.

Requirements

Functional

  • Acquire a named lock, waiting up to a timeout or failing at once.
  • Release a lock, but only if the caller holds it.
  • Release a lock automatically when its holder's session ends or its lease expires.
  • Return a fencing token with every successful acquire, higher than any earlier one for that lock.
  • Let a waiting client be told when a lock becomes free.

Non-functional

  • Safety: no two clients can both act on a resource as holder, because the resource rejects the older token.
  • Acquire and release: p99 under 20 ms inside one region.
  • Survive the loss of 2 of 5 servers without losing a lock, and recover from a leader failure in under 10 seconds.
  • Support 1 million held locks and 100,000 client sessions.

Step 2 of 6: Size it

Show the maths

These numbers are assumptions for the exercise, not facts about a real system:

  • 1 million locks are held at once by 100,000 client sessions. A lock entry (name, owner, token, expiry) takes about 100 bytes.
  • A session has a 10-second time to live, and clients send a heartbeat every 3 seconds.
  • The service runs on 5 servers. In one region a network round trip takes 1 ms and a disk sync takes 2 ms.

The arithmetic:

Lock table size
1,000,000 locks × 100 B
100 MB, held in memory on every server
Heartbeats if each lock had its own lease
1,000,000 locks ÷ 3 s
≈ 333,000/s, too many for a consensus leader
Heartbeats with one lease per session
100,000 sessions ÷ 3 s
≈ 33,000/s, ten times fewer
Time to commit a write
Followers: 0.5 ms there + 2 ms sync + 0.5 ms back (a 1 ms round trip) = 3 ms; the leader's own 2 ms sync runs in parallel
≈ 3 ms, well inside the 20 ms target
Quorum and failures
5 servers; a majority is 3, so up to 2 can fail
Writes need 3 of 5 servers, and 2 may be down
Heartbeats a client may miss
Heartbeats at 3, 6 and 9 s of a 10 s lease
2 in a row can be lost before the session expires

Step 3 of 6: Sketch the path of one request

Step through the diagram at the top of the page to watch a request move through these parts.

Clients talk to the Lock leader, one server in a group of five that agree on every change. A client opens a session with a 10-second lease and keeps it alive with a heartbeat every 3 seconds. All locks the client takes belong to its session.

To acquire a lock, the leader appends an entry to the Raft log, sends it to the Replicas and waits until three of the five servers hold it. Only then is it committed and applied to the Lock table. The grant carries a fencing token, a number that goes up with every grant of that lock. If the session stops sending heartbeats, its lease runs out, its locks are released and the next waiter gets the lock with a higher token.

The client sends the token with every request to the Guarded resource. The resource keeps the highest token it has seen and rejects anything lower. So a client that paused, woke up and still believes it holds the lock is stopped by the resource itself. The resource never calls the lock service, so it adds no new dependency to the hot path.

Other paths through the system

A paused client is fenced

  1. Client A holds the lock with token 8,412 and writes to the resource, sending the token with the write.
  2. The resource has seen no higher token, so it accepts the write and records 8,412 as the highest it has seen.
  3. Client A then freezes for 15 seconds, longer than its lease, so its heartbeats to the lock service stop.
  4. The lease runs out. The leader releases A's locks and grants the lock to client B with token 8,413.
  5. Client B writes to the resource with token 8,413. The resource accepts it and records 8,413.
  6. Client A wakes up, still believing it holds the lock, and writes with token 8,412.
  7. The resource has seen 8,413, which is higher, so it rejects the write from A. The stale holder does no harm.

Step 4 of 6: Go deep on the hard part

Why the lock state needs consensus

InterviewerWhy not keep the lock in one cache node with an expiring key? What does a replicated design buy you?The simplest lock is a key in a fast in-memory store: set it only if it does not exist, with an expiry.

The simplest lock is a key in a fast in-memory store: set it only if it does not exist, with an expiry. It is fine for saving effort, such as avoiding two copies of an email. It is not safe for protecting data. If that one node fails, every lock is gone. If you add a replica that copies the data asynchronously, a failover can promote a replica that never heard about a recent lock, and a second client acquires it while the first still believes it holds it. Two holders, with no error anywhere.

Schemes that take the lock on a majority of independent nodes and trust time limits improve on a single node, but their safety still depends on timing assumptions. A process pause or a clock jump can break them. When correctness matters, use agreement, not timing.

A consensus-replicated state machine. Run 3 or 5 servers that use a protocol such as Raft. One server is the leader. Each change (open a session, acquire, release, expire a session) is an entry in a log. The leader sends the entry to the others and counts it committed only when a majority has stored it on disk. Every server applies committed entries in the same order, so every copy of the lock table is the same. Any one failure leaves a majority alive, so nothing is lost.

Leaders can be wrong. A server that was the leader may be cut off from the others, while a new leader is elected by the majority. The old one cannot commit anything, because it cannot reach a majority. It must also not answer "this lock is free" from its stale copy, so lock reads go through the leader with a check that it still leads, or are themselves put through the log.

What it costs. Each write takes a round trip to a majority plus disk syncs, about 3 ms in a region. Adding servers does not add throughput. It makes each commit wait for more acknowledgements, and buys tolerance for more failures. Five servers tolerate two failures and three tolerate one. To scale, split lock names across several independent groups by a hash of the name, so a lock always lives in one group. Keep the group inside one region across three zones, since long distances make every commit slow.

Leases, sessions and clocks

InterviewerHow does a lock get released when its holder dies, and what do you assume about time?A lock with no expiry is held forever by a crashed client.

A lock with no expiry is held forever by a crashed client. So every lock is a lease: valid for a limited time and extended by the holder while it is alive.

Sessions. If each lock had its own lease, 1 million locks renewed every 3 seconds is about 333,000 renewals a second, each a write through the consensus leader. Instead, a client opens one session with a 10-second time to live, and all its locks are tied to it. One heartbeat renews them all, so the load is 100,000 sessions ÷ 3 s, about 33,000 a second. Heartbeats do not need a log entry each. The leader can keep session timers in memory and only write a log entry when a session is created or expires.

Who measures time. The leader times each session on its own monotonic clock, as the time since the last heartbeat. Nobody compares wall clocks between machines. The client does the same on its side: it counts 10 seconds on its own clock from the moment it sent the last heartbeat that succeeded, and subtracts a safety margin. The scheme assumes only that clocks tick at roughly the same speed, which is a much weaker claim than synchronised time.

A new leader. After a failover, the new leader does not know how long ago each session last sent a heartbeat. It gives every session a full fresh time to live. That can only make a session last longer than it should, never shorter, so a lock is never granted to a second client early. The cost is that a dead holder's locks may take up to 10 seconds longer to free.

Choosing the time to live. A short lease frees a dead holder's locks quickly, and it expires healthy clients that hit a garbage-collection pause or a network blip. A long lease is calmer and slower to recover. With a 10-second lease and 3-second heartbeats, a client can lose two heartbeats in a row and still renew.

A client that cannot renew in time must stop acting as the holder. Even so, no client can be fully trusted to do that, since a freeze can come right after it checks its lease and before it writes. That gap is what the next topic closes.

Fencing tokens

InterviewerA client acquires the lock, freezes for a long time, and wakes up after its lease has passed. How do you stop it from corrupting data?The danger is a gap between checking and acting.

The danger is a gap between checking and acting. Client A checks that it still holds the lock, then a garbage-collection pause, a stalled virtual machine or a slow network holds it for 15 seconds. Its lease expires, client B gets the lock and writes. Then A wakes up, believes it still has the lock, and writes over B's work. No amount of checking inside A can close this gap, because the freeze can happen between any two instructions.

The fix is to move the final check to the one place that cannot be paused out of order: the resource being protected. Every time the lock service grants a lock, it issues a fencing token, a number that increases with each grant of that lock. It can come from the log index or a counter kept with the lock entry. Because the log is ordered and replicated, tokens stay strictly increasing even across a leader change.

The client sends the token with every write. The resource remembers the highest token it has accepted for the object, and rejects any request with a lower one. In the example, A holds token 8,412 and B gets 8,413. When A's late write arrives with 8,412, the resource has seen 8,413 and refuses it.

What the resource needs. A way to compare and store a number atomically with the write. A database can do that with a conditional update that checks a version column. An object store can use a conditional write. A file service can keep the token in a small metadata field. The resource never contacts the lock service, so there is no extra failure path.

When fencing is not possible. Some resources cannot check a token, such as a third-party service. Then the lock only reduces the chance of two holders, and it should be used for efficiency, not correctness. Say so in the interview.

The same idea gives leader election. Use a lock named for the role, and treat the token as the leader's epoch. Followers and downstream systems reject any message carrying an older epoch.

Step 5 of 6: Name the trade-offs

Where lock state lives
Chose
A consensus-replicated group of 5
Over
One cache node with an expiring key

Locks survive failures and are never granted twice. The cost is about 3 ms of agreement on each write and the work of running a cluster.

Lease granularity
Chose
One lease per client session
Over
A lease on each lock

It cuts heartbeat load tenfold, and one failure releases all of a client's locks at once. The cost is that a client's locks all expire together.

Where safety is enforced
Chose
Fencing tokens checked by the resource
Over
Trusting the lease alone

A paused client cannot do harm, whatever the timing. The cost is that the resource must support a token check.

Cluster size
Chose
5 servers
Over
3 servers

Two servers can fail, for example one in maintenance and one in an outage. The cost is a slightly slower commit and more hardware.

Step 6 of 6: Say what breaks next

Follow-up questions an interviewer asks once the design is on the board. Answer each one out loud, then open it to compare.

What should a client do if it cannot reach the lock service?
Treat the lock as lost once its own lease time has passed, and stop the protected work. Continuing on the hope that nobody else got the lock is exactly the mistake fencing guards against. Retry with backoff until the service returns.
How would you queue waiters fairly and avoid a stampede on release?
Have each waiter add an entry with a sequence number under the lock's name. Each waiter watches only the entry just before its own. When that one goes away, the next in line wakes up, so a release wakes one client rather than all of them.
Can the lock service span regions?
A group across regions pays a long round trip for every commit, so it is slow. Most designs run one group per region and pick a home region for each lock. A cross-region lock is rare, and it accepts the extra latency.
Why not use clocks to expire locks and skip heartbeats?
Machines' clocks drift and can jump, so a lock that depends on synchronised time can expire early on one machine and late on another. Using each side's own monotonic timer and a margin avoids that. Fencing then covers whatever timing errors remain.

Free ATS check

See your resume the way an ATS reads it, then fix what it misses.

Drop in a PDF for a score out of 100, every fix that cost a point, and the line it belongs to. It runs in your browser, with no signup for the score.