Skip to content
/practice·distributed-lock

Distributed Lock

intermediatedistributed-lockredisconcurrencymutual-exclusion
/concept
Distributed Locking
Coordinating exclusive access across processes
Read full guide

Problem

Implement a distributed locking service using Redis. Your API should allow clients to acquire exclusive locks on named resources, perform work while holding the lock, and release it when done. If a client crashes while holding a lock, it should automatically expire so other clients aren't blocked forever.

Background

In distributed systems, multiple instances of a service often need to coordinate access to shared resources — a database row, an external API, a file, a scheduled job. Without coordination, two instances might process the same payment, send duplicate emails, or corrupt shared state.

A distributed lock solves this by providing mutual exclusion across processes. Redis is commonly used because it's fast, supports atomic operations, and can automatically expire keys (preventing deadlocks from crashed clients).

The key challenges are: ensuring only one client holds the lock at a time (mutual exclusion), making sure locks eventually expire (liveness), and ensuring only the lock owner can release it (safety).

Requirements

  • POST /lock/acquire — Acquire a named lock with a TTL
  • POST /lock/release — Release a lock (only if you own it)
  • GET /lock/status/:resource — Check if a resource is currently locked
  • Locks must be mutually exclusive — only one client holds a lock at a time
  • Locks must auto-expire after their TTL to prevent deadlocks
  • Only the lock owner (identified by their ownerId) can release the lock

Your Server

  • Your server receives PORT (default 3000) and REDIS_URL environment variables
  • Redis is provided as a sidecar — it's up when your server starts
  • Use Redis for all lock state — don't use in-memory maps

API Contract

`POST /lock/acquire`

Request body:

json
{
  class="text-pass">"resource": class="text-pass">"order-123",
  class="text-pass">"ownerId": class="text-pass">"worker-1",
  class="text-pass">"ttlMs": 5000
}

If lock acquired successfully:

  • Status: 200
  • Body:
json
{
  class="text-pass">"acquired": true,
  class="text-pass">"resource": class="text-pass">"order-123",
  class="text-pass">"ownerId": class="text-pass">"worker-1"
}

If lock is already held by another client:

  • Status: 409
  • Body:
json
{
  class="text-pass">"acquired": false,
  class="text-pass">"resource": class="text-pass">"order-123",
  class="text-pass">"holder": class="text-pass">"worker-2"
}

`POST /lock/release`

Request body:

json
{
  class="text-pass">"resource": class="text-pass">"order-123",
  class="text-pass">"ownerId": class="text-pass">"worker-1"
}

If lock released successfully:

  • Status: 200
  • Body: { "released": true }

If lock is held by a different owner:

  • Status: 403
  • Body: { "released": false, "error": "Lock held by different owner" }

If lock doesn't exist (already expired or never acquired):

  • Status: 404
  • Body: { "released": false, "error": "Lock not found" }

`GET /lock/status/:resource`

If locked:

  • Status: 200
  • Body:
json
{
  class="text-pass">"locked": true,
  class="text-pass">"resource": class="text-pass">"order-123",
  class="text-pass">"ownerId": class="text-pass">"worker-1"
}

If not locked:

  • Status: 200
  • Body:
json
{
  class="text-pass">"locked": false,
  class="text-pass">"resource": class="text-pass">"order-123"
}

Useful APIs

code
class=class="text-pass">"text-fg-subtle">// Redis: set a key only if it doesnclass="text-pass">'t exist (atomic lock acquire)
const result = await redis.set(key, value, 'PXclass="text-pass">', ttlMs, 'NXclass="text-pass">');
class=class="text-pass">"text-fg-subtle">// result === 'OKclass="text-pass">' if set, null if key already exists

class=class="text-pass">"text-fg-subtle">// Redis: get a value
const value = await redis.get(key);

class=class="text-pass">"text-fg-subtle">// Redis: delete a key only if it has a specific value (atomic lock release)
class=class="text-pass">"text-fg-subtle">// Use a Lua script for atomicity:
const script = class="text-pass">`
  if redis.call(class="text-pass">"get", KEYS[1]) == ARGV[1] then
    return redis.call(class="text-pass">"del", KEYS[1])
  else
    return 0
  end
`;
const result = await redis.eval(script, 1, key, expectedValue);
class=class="text-pass">"text-fg-subtle">// result === 1 if deleted, 0 if value didn't match

class=class="text-pass">"text-fg-subtle">// Redis: check TTL remaining on a key (milliseconds)
const ttl = await redis.pttl(key);
Scenarios·4 visible + hidden

Your solution will be tested against these scenarios plus a hidden set.

  1. 01Acquire a lock, perform work, and release it
  2. 02A second request is blocked while the lock is held
  3. 03Lock is automatically released after TTL expires
  4. 04A lock can only be released by the client that acquired it

Hints

Click each hint to reveal it. Take your time — try before you peek.