locking mechanisms
key acquisition
lock and key tutorial
security tips
locksmithing basics

How to acquire a lock by a key

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In software, "acquiring a lock by a key" usually means serializing work per logical identifier. For example, two threads processing the same customer ID should not run at the same time, but work for different customer IDs should still proceed concurrently.

That pattern is often called keyed locking or per-key locking. It is useful for caches, account updates, job deduplication, and any workflow where contention should be isolated to one specific resource instead of a single global mutex.

Why a Global Lock Is Often Too Coarse

A single global lock is easy to write, but it kills concurrency:

python
1import threading
2
3global_lock = threading.Lock()
4
5def process_account(account_id: str) -> None:
6    with global_lock:
7        print(f"Processing {account_id}")

This prevents races, but it also blocks unrelated accounts from running in parallel. If thread A is working on "acct-1", thread B handling "acct-2" is forced to wait even though there is no shared business conflict.

Keyed locking narrows the scope so only identical keys contend.

A Simple Keyed Lock in Python

One straightforward implementation uses:

  • a dictionary from keys to locks
  • a separate guard lock to protect that dictionary
python
1import threading
2from collections.abc import Hashable
3
4
5class KeyedLock:
6    def __init__(self) -> None:
7        self._locks: dict[Hashable, threading.Lock] = {}
8        self._guard = threading.Lock()
9
10    def acquire(self, key: Hashable) -> threading.Lock:
11        with self._guard:
12            lock = self._locks.setdefault(key, threading.Lock())
13        lock.acquire()
14        return lock
15
16
17keyed_lock = KeyedLock()
18
19def process_account(account_id: str) -> None:
20    lock = keyed_lock.acquire(account_id)
21    try:
22        print(f"Safely processing {account_id}")
23    finally:
24        lock.release()

With this design, calls for the same account_id serialize, while different account IDs can proceed independently.

A Context Manager Version

Returning a lock object works, but a context manager is easier to use correctly:

python
1import threading
2from contextlib import contextmanager
3from collections.abc import Hashable, Iterator
4
5
6class KeyedLock:
7    def __init__(self) -> None:
8        self._locks: dict[Hashable, threading.Lock] = {}
9        self._guard = threading.Lock()
10
11    @contextmanager
12    def hold(self, key: Hashable) -> Iterator[None]:
13        with self._guard:
14            lock = self._locks.setdefault(key, threading.Lock())
15
16        lock.acquire()
17        try:
18            yield
19        finally:
20            lock.release()
21
22
23keyed_lock = KeyedLock()
24
25with keyed_lock.hold("user-42"):
26    print("Only one worker may handle user-42 here")

This reduces the chance of forgetting to release the lock in an error path.

Important Design Detail: Cleaning Up Unused Locks

The simple dictionary approach can leak entries if you keep creating new keys forever. If your key space is unbounded, the map will grow without limit.

A production implementation often adds:

  • a reference count for active holders and waiters
  • removal of unused lock objects when the count drops to zero
  • time-based cleanup if keys are extremely dynamic

That bookkeeping is the hard part of keyed locking. The basic idea is easy; making it scale safely takes more care.

When to Use Semaphore Instead of Lock

Sometimes you want a per-key concurrency limit larger than one. In that case, use a keyed semaphore instead of a keyed mutex.

python
1import threading
2
3limiters: dict[str, threading.Semaphore] = {}
4guard = threading.Lock()
5
6def get_limiter(key: str) -> threading.Semaphore:
7    with guard:
8        return limiters.setdefault(key, threading.Semaphore(3))

That pattern allows up to three concurrent operations for the same key instead of exactly one.

Alternatives in Other Environments

The same concept appears in many ecosystems:

  • Java with ConcurrentHashMap<Key, ReentrantLock>
  • C# with ConcurrentDictionary<TKey, SemaphoreSlim>
  • distributed systems with Redis or database-backed locks

If work may happen on multiple machines, an in-process keyed lock is not enough. Then you need a distributed coordination mechanism keyed by the same logical resource ID.

Common Pitfalls

The biggest pitfall is forgetting to protect the dictionary of locks itself. If multiple threads create per-key locks without a guard, you can accidentally create two different lock objects for the same key and lose mutual exclusion.

Another mistake is never removing old keys. A keyed-lock table with an unbounded key space can become a memory leak over time.

Developers also sometimes use keyed locking when optimistic concurrency or atomic database updates would be simpler. A lock is not always the best fix.

Finally, an in-process keyed lock only protects one process. If multiple application instances can modify the same resource, you need a distributed locking or transaction strategy instead.

Summary

  • Keyed locking serializes work per logical resource instead of globally.
  • It is useful when identical keys must not overlap, but different keys may run in parallel.
  • A dictionary of per-key locks plus a guard lock is the basic implementation pattern.
  • Context-manager style APIs reduce release bugs.
  • Production implementations need a plan for cleaning up unused keys.
  • In multi-process or multi-node systems, use a distributed coordination mechanism instead of an in-memory keyed lock.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.