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:
Two requests come in at the same time to buy 3 units each:
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).
-
findByIdForUpdate runs → DB locks that row.
-
If another request tries to lock the same row:
-
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:
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:
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
3.2. Simple Update with Optimistic Locking
Scenario with concurrent updates:
🔹 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 throws OptimisticLockException / 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:
5. Spring Transaction & Locking Tips (Interview Friendly)
-
Locking works inside a transaction, so @Transactional is 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:
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.