Optimistic vs Pessimistic Locking: The System Design Interview Guide

June 3, 20269 min read
interview-prepcareersystem-designalgorithms
Optimistic vs Pessimistic Locking: The System Design Interview Guide
TL;DR
  • Optimistic locking lets multiple transactions read freely and checks for conflicts only at write time using a version column; if another writer committed first, the update matches zero rows and the caller retries.
  • Pessimistic locking acquires a row-level lock at read time via SELECT ... FOR UPDATE, making other transactions wait rather than fail, trading throughput for write predictability.
  • Contention rate is the deciding factor: optimistic locking wins below roughly 10–15% conflict probability; above that, retry loops cost more than locks would.
  • Retry storms are optimistic locking's failure mode under high concurrency — in a flash sale with thousands of simultaneous writers, failed retries spiral the system under load.
  • Deadlock is pessimistic locking's core failure mode when transactions lock multiple rows in inconsistent order; always acquire locks in a fixed, consistent order to prevent it.
  • In a distributed system, optimistic locking scales naturally across nodes while pessimistic locking requires a distributed lock service, adding latency and a new failure surface.
  • Idempotency keys should accompany any retry pattern so that failed-and-retried operations don't duplicate side effects like payments.

Optimistic vs Pessimistic Locking: two strategies, one shared row, infinite suffering

Optimistic vs Pessimistic Locking: The System Design Interview Guide

The optimistic vs pessimistic locking decision comes up in almost every system design interview that touches a shared resource. Most engineers know these two terms. Fewer can explain why one explodes under a flash sale and the other quietly deadlocks your bank service. Knowing which to reach for (and why) is worth more than knowing how to implement either.

The Core Bet Each Strategy Makes

Both strategies are trying to solve the same problem: two transactions want to write the same row. Someone has to lose. The strategies just disagree on when to figure that out.

Optimistic locking assumes conflicts are rare. It lets multiple transactions read freely, then checks at write time whether anyone else touched the data in the meantime. If yes, the write fails and the caller retries. Very zen. Very naive under a flash sale.

Pessimistic locking assumes conflicts are likely. It acquires a lock before reading, blocks everyone else, writes, then releases. No surprises at commit time, but everyone else waits in a line that feels like the DMV.

Neither strategy is "safer." They trade different failure modes. Optimistic trades write failures for throughput. Pessimistic trades throughput for write predictability. Your job in an interview is to know which tradeoff hurts less given the workload.

The Version Column Does All the Work

The canonical optimistic locking implementation uses a version column. Every row carries an integer that increments on every write. The mechanism is embarrassingly simple.

-- Step 1: Read the row and its version SELECT id, balance, version FROM accounts WHERE id = 123; -- Returns: balance=1000, version=7 -- Step 2: Modify in application memory, then write UPDATE accounts SET balance = 900, version = version + 1 WHERE id = 123 AND version = 7; -- <-- The entire trick -- If 0 rows affected: someone else wrote first. Retry.

The WHERE clause is the whole mechanism. If another transaction committed between your read and your write, the version will be 8, not 7. Your update matches zero rows. Your application detects this (rows affected = 0) and retries.

Transaction A                  Transaction B
READ row (version=7) ─────────────────────────────────────────>
                               READ row (version=7)
COMPUTE new value
                               COMPUTE new value
                               WRITE (version=7 → 8) ─ SUCCESS
WRITE (version=7) ─────────── 0 rows affected: CONFLICT
RETRY (read version=8) ──────────────────────────────────────>
WRITE (version=8 → 9) ─────── SUCCESS

No locks are held at the database level. The check is just a conditional write. This is why optimistic locking scales horizontally: each node reads and writes independently, and conflicts resolve at the application layer. The database has no idea any of this is happening.

Pessimistic Locking Blocks, It Doesn't Fail

Pessimistic locking uses SELECT ... FOR UPDATE to acquire a row-level lock at read time. Other transactions that try to read or write the same row block until the lock is released. They don't fail. They wait, patiently, like coworkers outside a single-stall bathroom.

BEGIN; -- Acquires an exclusive lock on this row immediately SELECT balance FROM accounts WHERE id = 123 FOR UPDATE; UPDATE accounts SET balance = 900 WHERE id = 123; COMMIT; -- Lock released here
Transaction A                  Transaction B
BEGIN
FOR UPDATE row 123 ──────────────────────────────────────────>
Lock acquired
                               BEGIN
                               FOR UPDATE row 123 ─ BLOCKED
READ balance (1000)
COMPUTE new value
WRITE balance = 900
COMMIT (lock released)
                               Lock acquired
                               READ balance (900)
                               COMPUTE new value
                               WRITE balance = 800
                               COMMIT

Transaction B does not fail. It waits. That is the fundamental difference: optimistic locking returns a conflict and asks the caller to retry. Pessimistic locking makes the caller wait transparently. Both get their write done eventually. They just have different feelings about the experience.

Most relational databases also support FOR SHARE, which allows multiple readers but blocks writers. Use it when you need a consistent read across multiple rows during a transaction without blocking other readers.

Contention Rate Is the Only Number That Matters

Here is the part most candidates skip, and it is the part that actually matters.

If the probability of two transactions conflicting on the same row is low, optimistic locking wins on throughput. Once contention climbs past roughly 10-15%, retry loops start hurting you more than locks would.

The math is simple. Say each transaction takes 10ms and a conflict forces one full retry. At 5% conflict rate, average transaction time is 10.5ms. Fine. At 50%, roughly half your transactions retry, and some retry multiple times. Average latency climbs past 15-20ms and variance explodes. Pessimistic locking at that point gives you 10ms with predictable queuing. Boring. Correct.

The other variable is retry cost. If retry involves network calls, side effects, or user-facing work, even a 2% conflict rate can hurt. If retry is cheap (re-read one row, re-apply a formula), you can tolerate higher conflict rates. The decision lives here, not in the abstract.

Where Each One Breaks

Optimistic locking has one failure mode: the retry storm. Picture a flash sale. 5,000 requests simultaneously try to decrement the last inventory slot. One succeeds. The other 4,999 fail and immediately retry. The next round looks almost identical. The system spirals under its own load instead of shedding it. Optimistic locking transfers the cost of contention from blocking to CPU and network. At high contention, that cost is significantly worse.

Pessimistic locking has two failure modes. First, deadlock: Transaction A locks row 1 and waits for row 2, while Transaction B locks row 2 and waits for row 1. Both wait forever. Databases detect this and kill one transaction, but it is still a failure your application must handle. The fix: always acquire locks in the same order. Second, lock contention at scale: pessimistic locking is a serialization point. In a distributed system with no shared lock manager, coordinating locks across nodes requires something like a distributed lock, which adds latency and a new failure surface.

Long transactions amplify both problems. The longer you hold a pessimistic lock, the more you block. The longer you hold data before writing it back, the higher your optimistic conflict rate. Both strategies hate long transactions and will let you know.

The Interview Scenarios That Always Come Up

These three cover about 80% of the locking questions you will see.

Bank transfer. Two accounts, a debit and a credit, must happen atomically. Use pessimistic locking with SELECT FOR UPDATE on both rows, acquiring locks in consistent account ID order to prevent deadlock. Optimistic locking is technically possible here but requires two retries and makes rollback logic messy. Pessimistic is the clean answer. Say it confidently.

Ticketmaster seat reservation. Two distinct phases. During browsing, users read inventory without locks. At checkout, the critical move is taking a seat from "available" to "reserved." A common approach: short-window pessimistic lock during payment, or a "soft reservation" row with a TTL confirmed via optimistic locking. The ticketmaster system design problem lives here. The interviewer is listening for whether you separate the read-heavy browse path from the write-critical checkout path. Most candidates miss this split.

Flash sale inventory deduction. 10,000 requests, 100 units. Optimistic locking is wrong here. The retry storm would be brutal. Pessimistic locking on the inventory row serializes writes and gives predictable behavior, but creates a bottleneck. The better answer is to pre-decrement into a Redis counter using atomic DECR (single-threaded, no coordination needed), accepting eventual consistency on the database side. If the interviewer constrains you to a relational database, pessimistic locking plus a queue is the right answer. Either way, do not start with optimistic.

Optimistic vs Pessimistic Locking: The Decision at a Glance

OptimisticPessimistic
Core assumptionConflicts rareConflicts likely
Lock timingAt writeAt read
Throughput (low contention)HighLower
Throughput (high contention)Degrades sharplyStable with queuing
Deadlock riskNoneYes, requires ordering
Retry handlingCaller retriesTransparent wait
Distributed systemsNatural fitRequires lock service
Long transactionsBad (conflict rate climbs)Bad (blocks escalate)

Talking Through the Decision in an Interview

Interviewers want reasoning, not a memorized answer. A frame that works:

  1. Ask about the access pattern. Read-heavy or write-heavy? How many concurrent writers realistically contend on the same row?
  2. Ask about the cost of a conflict. If a retry means re-running a payment, that is expensive. If it means re-reading a counter, it is cheap.
  3. Ask about the environment. Distributed microservices make pessimistic locking hard. A single PostgreSQL database makes it easy.
  4. State your choice and the tradeoff you are accepting. "I'll use optimistic locking because writes to user profiles are rare and contention is low. The tradeoff is that a conflict means a round trip to retry, which I'm comfortable with here."

That last step is what gets documented in your write-up. Picking correctly is table stakes. Explaining the tradeoff out loud is what pushes a hire decision to strong hire.

One More Pattern Worth Knowing

Idempotency keys pair well with both strategies when retries are involved. If your optimistic locking causes a retry that triggers a network call or a payment, you need to ensure the retry does not duplicate work. Idempotency keys let you safely retry without side effects. If your design involves retries for any reason, connecting these two mechanisms shows depth. More detail in the idempotency guide.


If you want to practice reasoning through locking tradeoffs out loud, the hardest part is not remembering the patterns. It is explaining your choice under pressure while an interviewer watches. SpaceComplexity runs voice-based system design mocks where you talk through exactly these tradeoffs and get rubric-based feedback on whether your reasoning was clear, complete, and correctly hedged.

Further Reading