Skip to main content
  1. Notes/

System Design

·5 mins·

Distribution Lock
#

Transactional Database & Distribution Lock & Design
#

  1. Push coordination into the transactional database - the @Version optimistic check or FOR UPDATE SKIP LOCKED from before. Postgres is already the single shared source of truth, and its row locks are visible to every connection from every instance.
  2. A distributed lock service — Redis (SET key NX PX, or Redisson’s RLock since you already use Redis), or Zookeeper. Justified when the critical section isn’t a database write — e.g., “only one instance should run this scheduled job.” For protecting a DB row it’s strictly worse: you’ve added a second system that can disagree with the first (lock expires mid-transaction, Redis failover loses the lock…).
  3. Single writer by design — route all spot-claims through one Kafka partition consumed by one consumer, so contention is eliminated by serialization rather than locking. Real pattern, but heavyweight for a parking lot; worth one sentence in an interview to show you know it exists.

In-JVM locks
#

  1. Pessimistic (parkPessimistic): one synchronized block makes find+claim atomic. Correct, simple, but every entry gate serializes through one lock. Mitigation: shard the lock per level.
  2. Optimistic (parkOptimistic): AtomicBoolean.compareAndSet — find a candidate, CAS-claim it, and if you lose the race, loop to the next free spot instead of blocking. No locks held during the search.
  3. The distributed punchline: in-JVM locks are useless once you run two Spring Boot instances.

Takeaway - Framing for interview
#

use a distributed lock when there is no shared transactional store already arbitrating the resource — coordinating side effects, singleton jobs, leader election. When the resource is a row in a database you’re already writing to, let that database be the arbiter; adding Redis on top is more infrastructure, more failure modes, and less correctness, because you’ve split one source of truth into two that can disagree.

Distribution Lock three examples
#

The three examples are exactly the cases where the resource being protected isn’t a row you’re transacting against, so the database has nothing to arbitrate:

Coordinating side effects. “Charge this customer’s card exactly once,” or “publish this event to an external API once.” The thing you’re guarding is an action against an outside system — Stripe, an email gateway. There’s no FOR UPDATE you can run on “the act of sending an email.” Postgres can’t lock an HTTP call. So you need an external arbiter to decide which instance performs the side effect. (Note: often the better fix here is idempotency keys rather than a lock — make the side effect safe to retry — but when you genuinely need at-most-once, a distributed lock is the tool.)

Singleton scheduled jobs. Three ECS instances all run @Scheduled(cron=…) and all fire at the top of the hour. The resource is “the right to run this job this hour.” That’s not a row — it’s a transient claim on an activity. No table is being contended; nothing for a DB row lock to grab. A distributed lock (or a DB-backed advisory lock used purely as a coordination token) is what lets exactly one instance proceed. This is the cleanest, most defensible use of RLock.

Leader election. “Exactly one instance is the primary / coordinator at a time.” Again the resource is a role, not data — “who is leader” is process-level state, not a row anyone’s updating. This is the canonical Zookeeper/etcd job, because their session-based ephemeral nodes release the role the instant the leader’s connection drops, which is more robust than a TTL guess.

The unifying principle: a lock has to live where all contenders can see it, and you don’t want two systems independently tracking the same truth. If the truth already lives in a transactional store (a row), that store should be the single arbiter — push the locking into it (FOR UPDATE, @Version, conditional UPDATE). If the truth isn’t in any shared store — it’s an external side effect, the right to run an activity, or a role — there’s nothing for the database to lock, so you introduce a dedicated coordination system to be that shared point of agreement.

A quick test you can apply in an interview: “Can I express this invariant as a constraint or conditional write on a row I’m already touching?” If yes, do that and skip the distributed lock. If no — because the contended thing isn’t data in your database — that’s your signal you genuinely need one.

Term
#

  1. Idempotency Key(Perform this specific side effect once. It doesn’t fit “only one instance should run this hourly job”): The key is something stable derived from the business operation — an order ID, not a random UUID generated fresh per attempt, or the dedup breaks.
  • A useful framing for interview: a lock is pessimistic coordination (stop others from acting), an idempotency key is optimistic plus reconciliation (let actions happen, collapse duplicates by identity). For external side effects over unreliable networks, the second survives failure modes that silently break the first.
  1. DB-backed advisory lock: using database’s lock manager. Not a row, not data — purely as the shared point of agreement for “who gets to do this.” It’s the database playing the role a Redis lock would play, but without the second-system drift problem