Optimistic vs Pessimistic Locking in Spring (with Multi-Instance Microservices)
When multiple users (or services) try to update the same data at the same time, we can easily end up with:
-
Lost updates
-
Inconsistent reads
-
Weird race conditions
To handle this, databases and ORMs provide locking mechanisms. In Spring/JPA, the two most common strategies are:
-
Pessimistic Locking – “Block others while I’m working”
-
Optimistic Locking – “Let others try, I’ll detect conflicts and retry”
In this post, we’ll cover:
-
The problem: concurrent updates
-
Pessimistic locking in Spring (with examples)
-
Optimistic locking in Spring (with examples)
-
How both behave when you have multiple instances of your Spring Boot service
-
When to choose which
1. The Problem: Concurrent Updates
Imagine a Product stock table:
| id | name | stock |
|---|---|---|
| 1 | iPhone | 10 |
Two requests come in at the same time to buy 3 units each:
-
Request A: reads stock = 10 → decides new stock = 7
-
Request B: reads stock = 10 → decides new stock = 7
If both update without any concurrency control, final stock will be 7 instead of 4 → ❌ lost update.
We need a way to coordinate writes.
2. Pessimistic Locking in Spring
๐ก Idea
“Lock now, work safely, then release.”
With pessimistic locking:
-
When one transaction reads a row for update, it locks that row at the database level.
-
Other transactions that try to lock the same row must wait (or fail with a timeout), until the first transaction commits or rolls back.
Under the hood, the DB does something like:
This row is locked until the transaction ends.
2.1. Entity Example
2.2. Repository with Pessimistic Lock
-
@Lock(LockModeType.PESSIMISTIC_WRITE)tells JPA/Hibernate to use a write lock. -
For supported databases (MySQL, Postgres, etc.), Hibernate will generate
SELECT ... FOR UPDATE.
2.3. Service: Decrease Stock with Pessimistic Lock
What happens at runtime?
-
Transaction starts (
@Transactional). -
findByIdForUpdateruns → DB locks that row. -
If another request tries to lock the same row:
-
It waits until the first transaction finishes
-
Or fails with a lock timeout if configured so.
-
-
First transaction updates and commits → lock is released.
-
Next transaction acquires the lock, reads latest value, proceeds.
This ensures no lost updates, but at the cost of:
-
Blocking
-
Possible deadlocks if you lock multiple rows in different orders
2.4. Pessimistic Locking with Multiple Instances
You might wonder: What if I run 5 instances of my Spring Boot microservice behind a load balancer?
Good news: pessimistic locking still works, because the locking is done at the database level, not in the Java process.
Sequence:
-
Instance A (Pod 1) starts a transaction, queries
SELECT ... FOR UPDATE. -
DB locks the row.
-
Instance B (Pod 2) tries same query → DB makes it wait.
-
When Instance A commits, DB releases the lock.
-
Instance B continues, reading the updated row.
So even with multiple instances:
-
All of them talk to the same database
-
The database is the single source of truth
-
Row locks are shared across connections/instances
Important:
To avoid long locks:
-
Keep transactions short
-
Don’t call external services inside a long-running transaction
-
Consider lock timeout settings (DB and JPA properties)
3. Optimistic Locking in Spring
๐ก Idea
“Assume no conflict; detect if it happens and retry.”
Optimistic locking is based on a version field:
-
When you read an entity, you read its version.
-
When you update, Hibernate generates SQL like:
If another transaction updated the same row meanwhile, the version changed → update affects 0 rows → Hibernate throws OptimisticLockException.
3.1. Entity with @Version
-
@Versionis the key here. -
Hibernate will automatically:
-
Populate it when first persisted
-
Increment it on each update
-
Use it in
WHEREclause on update
-
3.2. Simple Update with Optimistic Locking
Scenario with concurrent updates:
-
Initial state:
stock=10,version=1
๐น Transaction A:
-
Reads product → (stock=10, version=1)
-
Decreases stock → 7
-
On commit:
UPDATE ... SET stock=7, version=2 WHERE id=1 AND version=1;
→ Succeeds → row changes to(stock=7, version=2)
๐น Transaction B (started at same time):
-
Reads product → (stock=10, version=1)
-
Decreases stock → 7
-
On commit:
UPDATE ... SET stock=7, version=2 WHERE id=1 AND version=1;
→ Fails (because version is now 2, not 1)
→ Hibernate throwsOptimisticLockException/ObjectOptimisticLockingFailureException
So instead of silently overwriting, you get an exception.
3.3. Handling OptimisticLockException (Retry Pattern)
You usually wrap the logic with retry:
This is the common optimistic locking pattern:
-
Try to update
-
If conflict → catch exception
-
Reload latest data and retry (up to N times)
3.4. Optimistic Locking with Multiple Instances
Again, the concurrency control happens at the database level via the version column.
Instances don’t need to know about each other:
-
Instance A and B each read from DB and see the
version. -
On update, DB enforces that only one can successfully commit with that version.
-
The other instance gets an exception.
So optimistic locking:
-
Works fine in multi-instance microservice deployments
-
Is scalable, because you don’t block rows
-
Is great for read-heavy, low-contention scenarios
4. Pessimistic vs Optimistic Locking — When to Use What?
๐งช Quick Comparison
| Aspect | Pessimistic Locking | Optimistic Locking |
|---|---|---|
| Strategy | Lock row immediately | Allow concurrency, detect conflicts at commit |
| DB behavior | SELECT ... FOR UPDATE row locks | UPDATE ... WHERE version = ? check |
| Performance under high contention | Can cause blocking, deadlocks | Many retries / failures but no blocking |
| Good for | High-conflict updates, critical writes | Read-heavy workloads, rare conflicts |
| Multi-instance support | Yes (DB-level lock) | Yes (DB-level version check) |
| Failure mode | Lock timeout / deadlock | OptimisticLockException |
General guidelines:
-
Use pessimistic locking when:
-
Conflicts are very frequent
-
You’d rather block than retry
-
Operation is critical and you want strict serialization (e.g., bank transfers, inventory with tiny stock)
-
-
Use optimistic locking when:
-
Most of the time, conflicts don’t happen
-
You need high throughput
-
Occasional retries are acceptable
-
5. Spring Transaction & Locking Tips (Interview Friendly)
-
Locking works inside a transaction, so
@Transactionalis important. -
Default isolation in many databases with Spring is
READ_COMMITTED. -
Pessimistic locking relies on DB support for lock modes (e.g., MySQL InnoDB, Postgres).
-
Multiple instances do not break these strategies, because:
-
They share the same DB
-
Locks/versions are enforced in the DB, not in JVM memory.
-
6. Summary
-
Pessimistic locking:
“I will lock the row now so nobody else can change it while I’m working.”
✅ Safe, but can block and cause deadlocks. -
Optimistic locking:
“I assume no one else will change it; if they do, I’ll detect it and retry.”
✅ High throughput, but needs retry logic.
In distributed, multi-instance Spring Boot microservices:
-
Both work, as long as you rely on database-level concurrency control.
-
The database is your global lock manager.
No comments:
Post a Comment