Optimistic vs Pessimistic Locking in JPA and Hibernate
Complete guide to optimistic and pessimistic locking with concurrency problems, lost updates, @Version, LockModeType, Spring Data JPA examples, diagrams, SQL behavior, best practices, and interview questions.
What is Locking?
Locking is a technique used to handle concurrent access to the same database record.
Simple meaning:
Multiple users try to update the same row at the same time.
Locking decides who can update safely.
Why Locking is Needed
Imagine two users editing the same bank account balance.
Current Balance = 1000
User A:
Withdraw 100
User B:
Withdraw 200
If both read the same old balance and update independently, final balance can become wrong.
This problem is called:
Lost Update Problem
Lost Update Problem
flowchart TD
A["Balance = 1000"]
B["User A Reads 1000"]
C["User B Reads 1000"]
D["User A Updates Balance 900"]
E["User B Updates Balance 800"]
F["Final Balance 800"]
G["User A Update Lost"]
A --> B
A --> C
B --> D
C --> E
D --> F
E --> F
F --> G
Correct final balance should be:
700
But actual final balance becomes:
800
Because User B overwrites User A.
Two Main Locking Types
Optimistic Locking
Pessimistic Locking
High Level Difference
| Feature | Optimistic Locking | Pessimistic Locking |
|---|---|---|
| Assumption | Conflict is rare | Conflict is likely |
| DB Lock | No lock while reading | Row is locked |
| Performance | Better | Slower |
| Concurrency | High | Lower |
| Failure | Fails during commit | Waits or blocks |
| Best For | Read-heavy systems | Critical updates |
Real Life Analogy
Optimistic Locking
Google Docs version history
Many people can open the document.
System checks version before saving.
If version changed, conflict is detected.
Pessimistic Locking
Library book checkout
One person takes the book.
Others must wait until the book is returned.
Sample Entity
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
import jakarta.persistence.Table;
@Entity
@Table(name = "bank_accounts")
public class BankAccount {
@Id
private Long id;
private String accountNumber;
private Double balance;
@Version
private Integer version;
public BankAccount() {
}
public BankAccount(Long id, String accountNumber, Double balance) {
this.id = id;
this.accountNumber = accountNumber;
this.balance = balance;
}
// getters and setters
}
Optimistic Locking
What is Optimistic Locking?
Optimistic locking assumes:
Multiple users may read same data,
but update conflict is rare.
Hibernate does not lock the database row while reading.
Instead, it checks the entity version during update.
How Optimistic Locking Works
When entity is loaded:
id = 1
balance = 1000
version = 1
User A loads version 1.
User B also loads version 1.
User A updates first:
UPDATE bank_accounts
SET balance = 900,
version = 2
WHERE id = 1
AND version = 1;
Success.
User B updates later:
UPDATE bank_accounts
SET balance = 800,
version = 2
WHERE id = 1
AND version = 1;
Fails.
Because database version is already 2.
Hibernate throws:
OptimisticLockException
Optimistic Locking Diagram
sequenceDiagram
participant A as User A
participant B as User B
participant DB as Database
A->>DB: Read account version 1
B->>DB: Read account version 1
A->>DB: Update where version 1
DB-->>A: Success version becomes 2
B->>DB: Update where version 1
DB-->>B: Failed version mismatch
Optimistic Locking Code
@Service
public class AccountService {
private final BankAccountRepository bankAccountRepository;
public AccountService(BankAccountRepository bankAccountRepository) {
this.bankAccountRepository = bankAccountRepository;
}
@Transactional
public void withdraw(Long accountId, double amount) {
BankAccount account =
bankAccountRepository.findById(accountId)
.orElseThrow();
double newBalance = account.getBalance() - amount;
account.setBalance(newBalance);
// No save required if entity is managed.
// Dirty checking + @Version will update safely.
}
}
Generated SQL
SELECT *
FROM bank_accounts
WHERE id = 1;
During commit:
UPDATE bank_accounts
SET balance = ?,
version = ?
WHERE id = ?
AND version = ?;
Spring Data Repository
import org.springframework.data.jpa.repository.JpaRepository;
public interface BankAccountRepository
extends JpaRepository<BankAccount, Long> {
}
No special method needed for basic optimistic locking.
Only add:
@Version
private Integer version;
Handling OptimisticLockException
import jakarta.persistence.OptimisticLockException;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SafeAccountService {
private final BankAccountRepository repository;
public SafeAccountService(BankAccountRepository repository) {
this.repository = repository;
}
@Transactional
public void withdraw(Long accountId, double amount) {
try {
BankAccount account =
repository.findById(accountId).orElseThrow();
account.setBalance(account.getBalance() - amount);
} catch (ObjectOptimisticLockingFailureException ex) {
throw new RuntimeException(
"Account was updated by another transaction. Please retry.",
ex
);
}
}
}
Important:
In real applications, retry should usually happen outside the failed transaction.
Retry Example
@Service
public class AccountRetryService {
private final AccountService accountService;
public AccountRetryService(AccountService accountService) {
this.accountService = accountService;
}
public void withdrawWithRetry(Long accountId, double amount) {
int maxAttempts = 3;
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
accountService.withdraw(accountId, amount);
return;
} catch (ObjectOptimisticLockingFailureException ex) {
if (attempt == maxAttempts) {
throw ex;
}
}
}
}
}
Optimistic Locking Good Use Cases
✅ Product details update
✅ Customer profile update
✅ CMS/blog post editing
✅ Employee master data
✅ Read-heavy applications
✅ Low conflict systems
Optimistic Locking Bad Use Cases
❌ High-frequency account balance updates
❌ Flash sale inventory decrement
❌ Seat booking final confirmation
❌ Critical financial ledger updates
For these, pessimistic locking may be better.
Pessimistic Locking
What is Pessimistic Locking?
Pessimistic locking assumes:
Conflict is likely.
So lock the row before updating.
When one transaction locks a row, other transactions must wait.
Pessimistic Locking Diagram
sequenceDiagram
participant A as Transaction A
participant B as Transaction B
participant DB as Database
A->>DB: Select account for update
DB-->>A: Row locked
B->>DB: Select same account for update
DB-->>B: Wait
A->>DB: Update and commit
DB-->>A: Lock released
DB-->>B: Row available
B->>DB: Continue update
LockModeType Options
| Lock Mode | Meaning |
|---|---|
| PESSIMISTIC_READ | Shared read lock |
| PESSIMISTIC_WRITE | Exclusive write lock |
| PESSIMISTIC_FORCE_INCREMENT | Write lock and increment version |
| OPTIMISTIC | Optimistic version check |
| OPTIMISTIC_FORCE_INCREMENT | Optimistic check and increment version |
Pessimistic Write Lock Example
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface BankAccountRepository
extends JpaRepository<BankAccount, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select a from BankAccount a where a.id = :id")
Optional<BankAccount> findByIdForUpdate(Long id);
}
Service Using Pessimistic Lock
@Service
public class PessimisticAccountService {
private final BankAccountRepository repository;
public PessimisticAccountService(BankAccountRepository repository) {
this.repository = repository;
}
@Transactional
public void withdraw(Long accountId, double amount) {
BankAccount account =
repository.findByIdForUpdate(accountId)
.orElseThrow();
if (account.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
account.setBalance(account.getBalance() - amount);
}
}
Generated SQL
For many databases:
SELECT *
FROM bank_accounts
WHERE id = 1
FOR UPDATE;
Then:
UPDATE bank_accounts
SET balance = ?
WHERE id = ?;
Pessimistic Write Flow
flowchart TD
A["Transaction Starts"]
B["Select Row For Update"]
C["Database Locks Row"]
D["Modify Entity"]
E["Commit Transaction"]
F["Lock Released"]
A --> B
B --> C
C --> D
D --> E
E --> F
PESSIMISTIC_READ Example
Use when many transactions can read, but writes should be blocked.
@Lock(LockModeType.PESSIMISTIC_READ)
@Query("select a from BankAccount a where a.id = :id")
Optional<BankAccount> findByIdWithReadLock(Long id);
Meaning:
Other readers may continue.
Writers may wait.
Database behavior depends on the database engine.
PESSIMISTIC_FORCE_INCREMENT Example
Locks row and increments version.
@Lock(LockModeType.PESSIMISTIC_FORCE_INCREMENT)
@Query("select a from BankAccount a where a.id = :id")
Optional<BankAccount> findByIdAndIncrementVersion(Long id);
Use when:
You want pessimistic lock
+
version increment
EntityManager Lock Example
@Transactional
public void withdrawUsingEntityManager(Long accountId, double amount) {
BankAccount account =
entityManager.find(
BankAccount.class,
accountId,
LockModeType.PESSIMISTIC_WRITE
);
account.setBalance(account.getBalance() - amount);
}
Lock Timeout Example
Sometimes we do not want to wait forever.
import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.QueryHints;
@Lock(LockModeType.PESSIMISTIC_WRITE)
@QueryHints({
@QueryHint(
name = "jakarta.persistence.lock.timeout",
value = "3000"
)
})
@Query("select a from BankAccount a where a.id = :id")
Optional<BankAccount> findByIdForUpdateWithTimeout(Long id);
Meaning:
Wait maximum 3 seconds for lock.
If lock not available, fail.
Lock Timeout Flow
flowchart TD
A["Transaction B Requests Lock"]
B["Row Already Locked By Transaction A"]
C["Wait Up To 3 Seconds"]
D["Lock Available"]
E["Continue"]
F["Timeout"]
G["Throw Exception"]
A --> B
B --> C
C --> D
D --> E
C --> F
F --> G
Inventory Example
Entity
@Entity
@Table(name = "products")
public class Product {
@Id
private Long id;
private String name;
private Integer availableQuantity;
@Version
private Integer version;
// getters and setters
}
Optimistic Inventory Update
@Transactional
public void buyProductOptimistic(Long productId, int quantity) {
Product product =
productRepository.findById(productId)
.orElseThrow();
if (product.getAvailableQuantity() < quantity) {
throw new RuntimeException("Out of stock");
}
product.setAvailableQuantity(
product.getAvailableQuantity() - quantity
);
}
Works well when conflict is rare.
Pessimistic Inventory Update
public interface ProductRepository
extends JpaRepository<Product, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select p from Product p where p.id = :id")
Optional<Product> findByIdForUpdate(Long id);
}
@Transactional
public void buyProductPessimistic(Long productId, int quantity) {
Product product =
productRepository.findByIdForUpdate(productId)
.orElseThrow();
if (product.getAvailableQuantity() < quantity) {
throw new RuntimeException("Out of stock");
}
product.setAvailableQuantity(
product.getAvailableQuantity() - quantity
);
}
Better for flash sale or high-conflict stock updates.
Inventory Locking Diagram
flowchart TD
A["Product Quantity 1"]
B["User A Buy"]
C["User B Buy"]
D["Optimistic Locking"]
E["Both Read Quantity 1"]
F["One Update Succeeds"]
G["Other Fails Version Check"]
H["Pessimistic Locking"]
I["User A Locks Product"]
J["User B Waits"]
K["User A Buys"]
L["User B Checks Latest Quantity"]
M["Out Of Stock"]
A --> B
A --> C
B --> D
C --> D
D --> E
E --> F
F --> G
B --> H
C --> H
H --> I
I --> J
J --> K
K --> L
L --> M
Seat Booking Example
Entity
@Entity
@Table(name = "seats")
public class Seat {
@Id
private Long id;
private String seatNumber;
private boolean booked;
@Version
private Integer version;
// getters and setters
}
Pessimistic Seat Booking
public interface SeatRepository
extends JpaRepository<Seat, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select s from Seat s where s.id = :id")
Optional<Seat> findSeatForBooking(Long id);
}
@Transactional
public void bookSeat(Long seatId) {
Seat seat =
seatRepository.findSeatForBooking(seatId)
.orElseThrow();
if (seat.isBooked()) {
throw new RuntimeException("Seat already booked");
}
seat.setBooked(true);
}
This prevents two users from booking the same seat.
Optimistic vs Pessimistic Timeline
flowchart LR
A["Optimistic"]
B["Read Without Lock"]
C["Work"]
D["Check Version At Commit"]
E["Conflict Fails"]
F["Pessimistic"]
G["Lock Row First"]
H["Work"]
I["Commit"]
J["Release Lock"]
A --> B
B --> C
C --> D
D --> E
F --> G
G --> H
H --> I
I --> J
When to Choose Which?
| Scenario | Better Choice |
|---|---|
| User profile update | Optimistic |
| Blog post edit | Optimistic |
| Product catalog update | Optimistic |
| Bank withdrawal | Pessimistic |
| Seat booking | Pessimistic |
| Flash sale inventory | Pessimistic |
| Mostly read system | Optimistic |
| High conflict writes | Pessimistic |
Common Exceptions
Optimistic Locking
OptimisticLockException
ObjectOptimisticLockingFailureException
StaleObjectStateException
Pessimistic Locking
PessimisticLockException
LockTimeoutException
CannotAcquireLockException
DeadlockLoserDataAccessException
Deadlock Risk in Pessimistic Locking
Deadlock can happen when transactions lock rows in different order.
Transaction A:
Locks Account 1
Waits for Account 2
Transaction B:
Locks Account 2
Waits for Account 1
Deadlock.
Deadlock Diagram
flowchart TD
A["Transaction A Locks Account 1"]
B["Transaction B Locks Account 2"]
C["Transaction A Waits For Account 2"]
D["Transaction B Waits For Account 1"]
E["Deadlock"]
A --> C
B --> D
C --> E
D --> E
Avoiding Deadlocks
✅ Lock rows in same order
✅ Keep transactions short
✅ Add lock timeout
✅ Avoid user interaction inside transaction
✅ Avoid external API calls inside transaction
✅ Use indexes for locked queries
Money Transfer Example
@Transactional
public void transfer(Long fromId, Long toId, double amount) {
Long firstId = Math.min(fromId, toId);
Long secondId = Math.max(fromId, toId);
BankAccount first =
repository.findByIdForUpdate(firstId).orElseThrow();
BankAccount second =
repository.findByIdForUpdate(secondId).orElseThrow();
BankAccount from =
first.getId().equals(fromId) ? first : second;
BankAccount to =
first.getId().equals(toId) ? first : second;
if (from.getBalance() < amount) {
throw new RuntimeException("Insufficient balance");
}
from.setBalance(from.getBalance() - amount);
to.setBalance(to.getBalance() + amount);
}
Why sort IDs?
Always lock accounts in same order.
This reduces deadlock risk.
Full Decision Diagram
flowchart TD
A["Need To Update Same Data Concurrently"]
B["Conflict Rare"]
C["Use Optimistic Locking"]
D["Conflict Frequent"]
E["Use Pessimistic Locking"]
F["Need Strict Sequential Updates"]
G["Use PESSIMISTIC_WRITE"]
H["Read Heavy Entity"]
I["Use Version Field"]
A --> B
B --> C
A --> D
D --> E
E --> F
F --> G
C --> H
H --> I
Best Practices
✅ Use @Version for most entities that can be updated by users
✅ Use optimistic locking by default for read-heavy systems
✅ Use pessimistic locking only for high-conflict critical updates
✅ Keep pessimistic transactions very short
✅ Add lock timeout for pessimistic locks
✅ Lock rows in consistent order
✅ Handle retry for optimistic conflicts
❌ Do not hold DB locks during external API calls
❌ Do not use pessimistic locking everywhere
❌ Do not ignore optimistic locking exceptions
Interview Questions
Q1. What is optimistic locking?
Optimistic locking uses a version column to detect concurrent updates during commit.
Q2. What is pessimistic locking?
Pessimistic locking locks the database row before update so other transactions wait.
Q3. How do we enable optimistic locking in JPA?
@Version
private Integer version;
Q4. Which SQL is generated by optimistic locking?
UPDATE table
SET data = ?, version = ?
WHERE id = ?
AND version = ?;
Q5. When should we use pessimistic locking?
When conflicts are frequent and update correctness is critical, such as inventory, seat booking, and financial balance updates.
Q6. What is PESSIMISTIC_WRITE?
It creates an exclusive database lock, usually using SELECT FOR UPDATE.
Q7. What is the main risk of pessimistic locking?
Deadlocks
Long waits
Reduced throughput
Q8. What is the main risk of optimistic locking?
Update failure during commit
Need retry handling
Summary
Locking protects data from concurrent update problems.
Optimistic Locking
No DB lock during read
Uses version check during update
Best when conflict is rare
Pessimistic Locking
Locks DB row before update
Other transactions wait
Best when conflict is likely
Simple rule:
Use Optimistic Locking by default.
Use Pessimistic Locking for critical high-conflict updates.
For most enterprise Spring Boot applications:
@Version
+
@Transactional
+
Proper exception handling
is the best starting point.