Types of Locks Explained: Optimistic vs. Pessimistic Locking
How do you prevent two users from editing the same record at the same time? We compare Optimistic Locking (Versioning) and Pessimistic Locking (Databa
Abstract Algorithms
TLDR: Pessimistic locking locks the record before editing โ safe but slower under low contention. Optimistic locking checks for changes before saving using a version number โ fast but can fail and require retry under high contention. Choosing correctly depends on your write-conflict probability.
๐ Two Ways to Prevent the Concurrent Edit Problem
Imagine a Shared Google Doc. Two people open it simultaneously and both start typing.
- Pessimistic approach: Only one person can type at a time. The doc is "locked" for everyone else until the first person finishes.
- Optimistic approach: Both people can type freely. When someone saves, the system checks if the doc changed while they were typing. If it did, they get a conflict error and must retry.
Neither approach is universally better. The right choice depends on how often write conflicts actually occur in your system.
| Strategy | Lock timing | Performance | Best when |
| Pessimistic | Before reading | Slower (blocks others) | High conflict probability |
| Optimistic | At save time (version check) | Faster (no blocking) | Low conflict probability |
๐ Locking Fundamentals: Concurrency and Write Conflicts
Before picking a locking strategy, it helps to understand exactly what can go wrong without one. When two transactions read and write the same row at overlapping times, the database can produce incorrect results โ silently. These are called concurrency anomalies.
| Anomaly | What happens | Example |
| Lost update | Both transactions read the same value; each writes back an increment โ one write is overwritten | Two cashiers both read stock = 10 and both sell 1; stock ends up at 9 instead of 8 |
| Dirty read | Transaction B reads a value written by Transaction A before A commits; A then rolls back | B sees a balance of $500 that A wrote but later reversed |
| Phantom read | Transaction B re-reads a range and gets extra rows that Transaction A inserted mid-query | A report counts 100 orders on first scan, 102 on the second |
Locking prevents these anomalies by serialising access to shared data. Pessimistic locking prevents them by refusing to let a second transaction even see the row until the first is done. Optimistic locking prevents the lost update specifically by detecting that the row changed and refusing to commit the stale write.
Understanding which anomaly you are actually protecting against is the first step to choosing the right lock.
๐ข Pessimistic Locking: SELECT ... FOR UPDATE
Pessimistic locking acquires an exclusive database lock on a row before any modification.
-- Transaction A: lock the row
BEGIN TRANSACTION;
SELECT * FROM inventory WHERE product_id = 42 FOR UPDATE;
-- Now safely modify
UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 42;
COMMIT TRANSACTION;
While Transaction A holds the lock, Transaction B's SELECT ... FOR UPDATE on the same row blocks until A commits or rolls back.
sequenceDiagram
participant A as Transaction A
participant B as Transaction B
participant DB as Database
A->>DB: BEGIN + SELECT FOR UPDATE (product 42)
DB-->>A: Row locked
B->>DB: SELECT FOR UPDATE (product 42)
DB-->>B: Waiting for lock...
A->>DB: UPDATE + COMMIT
DB-->>B: Lock acquired
B->>DB: UPDATE + COMMIT
๐ Lock State Machine
stateDiagram-v2
[*] --> Unlocked
Unlocked --> Locked_T1 : Thread T1 acquires
Locked_T1 --> Contended : Thread T2 requests
Contended --> T2_Waiting : T2 blocked
T2_Waiting --> Locked_T2 : T1 releases lock
Locked_T2 --> Unlocked : T2 releases
Locked_T1 --> Unlocked : T1 releases (no contention)
Lock types:
| Lock | Behavior |
| Exclusive (X) | Only one holder; blocks all reads and writes |
| Shared (S) | Multiple holders allowed for reads; blocks exclusive locks |
Deadlock risk: Transaction A holds Row 1, wants Row 2. Transaction B holds Row 2, wants Row 1. Both wait forever. Databases detect this and roll back one transaction. Mitigation: always acquire locks in a consistent order; keep transactions short.
โ๏ธ Optimistic Locking: Version Column Pattern
Optimistic locking adds a version column. No lock is held during the read โ only at write time does the system check whether anyone else modified the row first.
-- Step 1: Read with version
SELECT id, balance, version FROM accounts WHERE id = 1;
-- Returns: { id: 1, balance: 1000, version: 3 }
-- Step 2: Update with version check
UPDATE accounts
SET balance = 900, version = 4 -- increment version
WHERE id = 1 AND version = 3; -- only if version unchanged
-- Step 3: Check affected rows
-- 1 row affected = success
-- 0 rows affected = conflict โ retry
In Java with JPA:
@Entity
public class Account {
@Id Long id;
double balance;
@Version // JPA manages optimistic locking automatically
int version;
}
JPA throws OptimisticLockException when the version check fails โ the calling service must catch it and retry.
๐ง Deep Dive: How Database Locks Are Implemented
Databases implement pessimistic locks via a lock manager: an in-memory hash table keyed by (table, row) that tracks which transaction holds each lock and which are waiting. The lock manager periodically scans wait queues for cycles to detect deadlocks, then rolls back one transaction as the victim. Optimistic locking needs no lock manager โ the version check happens inside the UPDATE statement itself, handled entirely by the storage engine at commit time.
๐ Choosing Your Lock Strategy
The decision between pessimistic and optimistic locking is primarily a question of conflict frequency. Use the flowchart below as a quick decision guide, then validate against your observed write-conflict rate in production.
flowchart TD
A[Write operation needed] --> B{How frequent are write conflicts?}
B -->|High - more than 10 percent| C[Use Pessimistic Locking]
B -->|Low - less than 5 percent| D[Use Optimistic Locking]
C --> E{Single row or multiple rows?}
E -->|Single row| F[SELECT FOR UPDATE]
E -->|Multiple rows| G[Lock in consistent order to prevent deadlock]
D --> H[Add version column to table]
H --> I{Conflict detected on save?}
I -->|Yes| J[Throw OptimisticLockException - retry]
I -->|No| K[Commit successfully]
๐ Lock Type Selection Decision Tree
flowchart TD
A[Choose a lock strategy] --> B{Concurrent reads frequent?}
B -- Yes --> C{Writes also frequent?}
C -- No --> D[ReadWriteLock\nshared reads / exclusive writes]
C -- Yes --> E[Pessimistic row lock]
B -- No --> F{Single-threaded access?}
F -- Yes --> G[No lock needed]
F -- No --> H{Low conflict probability?}
H -- Yes --> I[Optimistic MVCC\nversion column]
H -- No --> J[Pessimistic lock\nSELECT FOR UPDATE]
If your write-conflict rate sits between 5 % and 10 %, measure under realistic load before deciding. Optimistic locking's retry overhead compounds quickly when conflicts are frequent, so a brief load test is worth the effort before committing to a strategy.
๐ Real-World Applications: Choosing the Right Strategy for Real Systems
| Scenario | Best choice | Why |
| Inventory decrement (flash sale, limited stock) | Pessimistic | Two buyers cannot be allowed to buy the last item concurrently |
| Banking transfer between two accounts | Pessimistic | Atomic debit + credit must be consistent |
| User profile update (low concurrent edit) | Optimistic | Conflicts are rare; version check is sufficient |
| CMS article editing (one author at a time likely) | Optimistic | Most edits are by different users; blocking is unnecessary overhead |
| Read-heavy + occasional write | Optimistic | Avoids locking on all those reads |
โ๏ธ Trade-offs & Failure Modes: Trade-offs and Failure Modes
| Failure mode | Strategy | Symptom | Mitigation |
| Deadlock | Pessimistic | Both transactions wait forever | Consistent lock order; short transactions; timeout + retry |
| Starvation | Pessimistic | Low-priority transaction never gets the lock | Fairness algorithms; lock queuing |
| Lost update | Optimistic (if not implemented) | Second writer silently overwrites first | Add version column; throw on conflict |
| Retry storm | Optimistic | High contention causes endless retries | Exponential backoff; switch to pessimistic for hot rows |
| Lock escalation | Pessimistic | Row locks escalate to table lock | Avoid locking large result sets; use indexes |
The probability of lock contention under random access can be modeled as:
$$P( ext{conflict}) pprox 1 - e^{-\lambda t}$$
where $\lambda$ is the arrival rate of writers and $t$ is the average transaction duration. When $\lambda t$ is small (< 0.1), optimistic is clearly cheaper. When $\lambda t > 1$, pessimistic avoids the retry overhead.
๐งญ Decision Guide: Pessimistic vs. Optimistic
| Question | Pessimistic wins when... | Optimistic wins when... |
| How often do write conflicts occur? | Frequently (> 10% of writes) | Rarely (< 5% of writes) |
| How long are critical sections? | Short (single DB transaction) | Doesn't matter โ no blocking |
| Can you tolerate retry logic in the app? | No โ retry adds complexity | Yes โ simple retry loop is acceptable |
| Is the record accessed by many users? | Yes (hot row) | No (user-private data) |
๐ฏ What to Learn Next
๐งช Lock Strategy in Practice: Three Scenarios
Theory only goes so far โ the best way to build intuition is to walk through real-world situations and reason through the choice. Each scenario below was selected to represent a distinct point on the conflict-probability spectrum, from near-certain concurrent writes to rare cross-user collisions. As you read each row, focus on the reasoning column: notice how the same two questions โ "how hot is this row?" and "what is the cost of a conflict?" โ drive every recommendation.
| Scenario | Recommended strategy | Reasoning |
| E-commerce flash sale (100 users buy the last item simultaneously) | Pessimistic (SELECT FOR UPDATE) | Multiple transactions must not see the same stock count concurrently. A lost update means overselling. The row is hot and conflicts are near-certain. |
| Banking account transfer (debit account A, credit account B atomically) | Pessimistic (lock both rows in consistent ID order) | Debit and credit are an atomic pair. Neither can be allowed to proceed on a stale balance. Lock order (lower ID first) prevents deadlocks. |
| CMS article draft (authors editing their own articles) | Optimistic (version column) | Each author edits their own article; simultaneous edits on the same draft are rare. Blocking would add unnecessary overhead for a very unlikely event. A conflict error is easy to surface in the UI ("Someone else saved changes โ please review and retry"). |
The pattern: if your scenario involves a shared hot resource (last item, shared balance), pessimistic wins. If each user mostly touches their own data with only occasional overlap, optimistic wins and is cheaper to operate at scale.
๐ ๏ธ Spring @Transactional and java.util.concurrent: Locks in Production Java Code
Spring Framework's @Transactional maps directly onto the pessimistic/optimistic locking strategies in this post โ it translates @Lock(LockModeType.PESSIMISTIC_WRITE) into a SELECT ... FOR UPDATE and @Version into the optimistic version-check pattern, so you never write raw SQL locking statements in a Spring application. java.util.concurrent provides ReentrantLock, ReadWriteLock, and StampedLock for in-process concurrent access control that mirrors the same trade-offs at the JVM level.
import jakarta.persistence.*;
import org.springframework.data.jpa.repository.*;
import org.springframework.transaction.annotation.*;
import java.util.concurrent.locks.*;
// โโ JPA Optimistic Locking: @Version column โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@Entity
public class Inventory {
@Id Long productId;
int quantity;
@Version // JPA manages version check automatically on UPDATE
int version; // throws OptimisticLockException when stale
}
// โโ JPA Pessimistic Locking: SELECT โฆ FOR UPDATE via @Lock โโโโโโโโโโโโโโโโโโโ
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE) // โ SELECT โฆ FOR UPDATE
@Query("SELECT i FROM Inventory i WHERE i.productId = :id")
Inventory lockById(@Param("id") Long id);
}
// โโ Service layer using Spring @Transactional โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@Service
public class InventoryService {
private final InventoryRepository repo;
public InventoryService(InventoryRepository repo) { this.repo = repo; }
// Flash sale: pessimistic lock prevents overselling
@Transactional
public void decrementStock(Long productId, int qty) {
Inventory inv = repo.lockById(productId); // holds row lock until commit
if (inv.quantity < qty) throw new IllegalStateException("Insufficient stock");
inv.quantity -= qty;
repo.save(inv);
}
// Profile update: optimistic lock โ rare conflicts, no blocking
@Transactional
public void updateProfile(Inventory item) {
// JPA auto-checks @Version on save; throws OptimisticLockException on conflict
repo.save(item);
}
}
// โโ In-process ReadWriteLock: multiple readers, exclusive writer โโโโโโโโโโโโโโ
public class CacheManager {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Map<String, String> cache = new HashMap<>();
public String read(String key) {
rwLock.readLock().lock(); // many concurrent readers allowed
try { return cache.get(key); }
finally { rwLock.readLock().unlock(); }
}
public void write(String key, String value) {
rwLock.writeLock().lock(); // exclusive โ all readers block during write
try { cache.put(key, value); }
finally { rwLock.writeLock().unlock(); }
}
}
// โโ StampedLock: optimistic read (no blocking), upgradeable to write โโโโโโโโโโ
public class PriceCache {
private final StampedLock sl = new StampedLock();
private double price = 0.0;
public double readPrice() {
long stamp = sl.tryOptimisticRead(); // no lock acquired
double p = price;
if (!sl.validate(stamp)) { // writer intervened โ upgrade to read lock
stamp = sl.readLock();
try { p = price; }
finally { sl.unlockRead(stamp); }
}
return p;
}
public void setPrice(double newPrice) {
long stamp = sl.writeLock();
try { price = newPrice; }
finally { sl.unlockWrite(stamp); }
}
}
StampedLock.tryOptimisticRead() is the in-process equivalent of optimistic locking: no lock is held during the read, and validate() checks whether a write occurred โ matching the version-column pattern exactly. Use it when read throughput vastly outweighs write frequency.
For a full deep-dive on Spring @Transactional and java.util.concurrent, a dedicated follow-up post is planned.
๐ Key Takeaways on Lock Selection
Choosing the wrong lock strategy leads to either lost updates (under-locking) or throughput bottlenecks (over-locking). Keep these four rules of thumb handy:
- Measure conflict probability first. Don't guess. Add a counter to your retry catch block and monitor it for one week under real load. If fewer than 5 % of writes ever retry, optimistic is almost always the right call.
- Pessimistic locking is safe by default, not fast by default. It prevents all write anomalies, but every held lock is a potential queue. Keep pessimistic transactions as short as possible โ read, compute, write, commit.
- Optimistic locking shifts the cost to the application layer. You trade database lock contention for retry-loop complexity. Make sure your service layer has exponential backoff and a maximum retry budget.
- Deadlocks are a pessimistic-locking tax. They are preventable but not eliminable. Always document the lock acquisition order for multi-row transactions in your team's conventions.
๐ TLDR: Summary & Key Takeaways
- Pessimistic locking blocks concurrent access with
SELECT ... FOR UPDATEโ safe under high contention. - Optimistic locking uses a version column โ no blocking, but requires retry logic on conflict.
- Deadlocks are the primary failure mode of pessimistic locking; retry storms affect optimistic locking under high contention.
- Always acquire locks in a consistent order to prevent deadlocks.
- Model your write-conflict probability before choosing: $P( ext{conflict}) pprox 1 - e^{-\lambda t}$.
๐ Practice Quiz
What is the primary advantage of optimistic locking over pessimistic locking?
- A) Optimistic locking never requires retries
- B) Optimistic locking avoids blocking other transactions during reads, improving throughput under low contention
- C) Optimistic locking prevents all write conflicts
- D) Optimistic locking is always faster than pessimistic locking regardless of conflict rate
Correct Answer: B โ Optimistic locking does not hold a database lock during the read phase, so other transactions can proceed freely; this is a clear throughput win when write-conflict probability is low.
A deadlock occurs in a pessimistic locking system. What is the most common detection mechanism?
- A) The database logs the error and ignores it
- B) The database detects the wait cycle and rolls back one of the transactions
- C) The application times out after 30 seconds
- D) The database escalates to a table lock to resolve the conflict
Correct Answer: B โ Databases use cycle-detection algorithms on the wait-for graph; when a cycle is found, one transaction is chosen as the victim and rolled back to break the deadlock.
When should you prefer pessimistic locking for inventory management?
- A) When users rarely edit the same product at the same time
- B) When multiple buyers can simultaneously attempt to purchase the last unit and you cannot allow overselling
- C) When read performance is the top priority
- D) When you want to avoid adding a version column to the table
Correct Answer: B โ With pessimistic locking the row is locked before the read, so no two buyers can see the same inventory count simultaneously; this is the only reliable way to guarantee no overselling.
๐ Related Posts

Written by
Abstract Algorithms
@abstractalgorithms
More Posts

Adapting to Virtual Threads for Spring Developers
TLDR: Platform threads (one OS thread per request) max out at a few hundred concurrent I/O-bound requests. Virtual threads (JDK 21+) allow millions โ with zero I/O-blocking cost. Spring Boot 3.2 enables them with a single property. Avoid synchronized...

Java 8 to Java 25: How Java Evolved from Boilerplate to a Modern Language
TLDR: Java went from the most verbose mainstream language to one of the most expressive. Lambdas killed anonymous inner classes. Records killed POJOs. Virtual threads killed thread pools for I/O work.
Data Anomalies in Distributed Systems: Split Brain, Clock Skew, Stale Reads, and More
TLDR: Distributed systems produce anomalies not because the code is buggy โ but because physics makes it impossible to be perfectly consistent, available, and partition-tolerant simultaneously. Split brain, stale reads, clock skew, causality violatio...
Sharding Approaches in SQL and NoSQL: Range, Hash, and Directory-Based Strategies Compared
TLDR: Sharding splits your database across multiple physical nodes so no single machine carries all the data or absorbs all the writes. The strategy you choose โ range, hash, consistent hashing, or directory โ determines whether range queries stay ch...
