How to Implement Optimistic Locking in JPA
Optimistic locking is a concurrency control strategy that allows multiple transactions to read the same data but prevents conflicting updates through version checking.
Overview
Optimistic locking is a concurrency control strategy that allows multiple transactions to read the same data but prevents conflicting updates through version checking.
Junior Level
What is Optimistic Locking
Optimistic locking allows multiple transactions to read the same data, but on update, checks that the data was not modified by another transaction.
Implemented via the @Version annotation:
@Entity
public class Order {
@Id
private Long id;
@Version
private Integer version; // Hibernate auto-increments
private String status;
}
How It Works
// Transaction 1
Order order1 = em.find(Order.class, 1L); // version = 1
// Transaction 2
Order order2 = em.find(Order.class, 1L); // version = 1
order2.setStatus("shipped");
em.flush(); // version = 2, UPDATE WHERE version = 1
// Transaction 1
order1.setStatus("cancelled");
em.flush(); // OptimisticLockException!
// UPDATE WHERE version = 1 -> 0 rows updated -> exception
Why “Optimistic”
Because we optimistically assume that conflicts won’t occur, and don’t lock data for reading. Conflict is detected only at write time.
Middle Level
LockModeType for Optimistic Locking
// OPTIMISTIC - version check on commit/flush
em.lock(order, LockModeType.OPTIMISTIC);
// OPTIMISTIC_FORCE_INCREMENT - always increments version
em.lock(order, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
// Even if order didn't change, version will increment
Handling OptimisticLockException
OptimisticLockException contains information about the entity and expected/actual version. In logs: “Row was updated or deleted by another transaction”.
try {
order.setStatus("cancelled");
entityManager.flush();
} catch (OptimisticLockException e) {
// Refresh data from DB and retry
entityManager.refresh(order);
// retry logic
order.setStatus("cancelled");
entityManager.flush();
}
Retry with Spring
@Retryable(value = OptimisticLockException.class, maxAttempts = 3)
@Transactional
public Order updateOrder(OrderDto dto) {
Order order = entityManager.find(Order.class, dto.id());
order.setStatus(dto.status());
return order;
}
Common Mistakes
// No @Version - no optimistic locking
@Entity
public class Order {
// no @Version -> conflicts not detected
}
// Manually changing version
order.setVersion(0); // breaks the mechanism
// Ignoring OptimisticLockException
try {
em.flush();
} catch (OptimisticLockException e) {
// silently ignored -> data lost
}
Senior Level
Internal Implementation
@Version -> version column in DB
On UPDATE:
UPDATE orders SET status = ?, version = version + 1
WHERE id = ? AND version = ?
If 0 rows updated -> OptimisticLockException
Process:
1. On INSERT: version = 0 (for Integer/Long). On first UPDATE: version = 1.
2. On UPDATE: version = version + 1
3. WHERE clause includes version check
4. If version doesn't match -> 0 rows -> exception
Version Field Types
@Version
private Integer version; // int - auto-increments
@Version
private Long version; // long - auto-increments
@Version
private Timestamp version; // timestamp - auto-updates
When to Use OPTIMISTIC_FORCE_INCREMENT
// When you need to increment version without changing the entity
// For example, when child collection changes
@Entity
public class Order {
@Version
private Integer version;
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
}
// When items change - need to increment Order version
em.lock(order, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
// Order version increments, although Order itself didn't change
Production Patterns
// Pattern 1: Retry with exponential backoff
@Retryable(
value = OptimisticLockException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 100, multiplier = 2)
)
@Transactional
public Order updateWithRetry(OrderDto dto) {
Order order = entityManager.find(Order.class, dto.id());
order.setStatus(dto.status());
return order;
}
// Pattern 2: Manual refresh and retry
@Transactional
public Order updateWithManualRetry(OrderDto dto) {
int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
try {
Order order = entityManager.find(Order.class, dto.id());
order.setStatus(dto.status());
entityManager.flush();
return order;
} catch (OptimisticLockException e) {
if (i == maxRetries - 1) throw e;
entityManager.clear(); // clear and try again
}
}
throw new IllegalStateException("Unreachable");
}
Optimistic vs Pessimistic
| Optimistic | Pessimistic | |
|---|---|---|
| Locking | No (only on write) | Yes (on read) |
| Conflict | Detected on write | Prevented on read |
| Performance | High (no locks) | Lower (locks) |
| Deadlock | Impossible | Possible |
| When | Rare conflicts | Frequent conflicts |
Best Practices
@Version on all mutable entities
Retry on OptimisticLockException
OPTIMISTIC_FORCE_INCREMENT when needed
@Retryable for automatic retry
Monitor conflict frequency
Without @Version for important data
Without retry logic
Manual version changes
Pessimistic locking without reason
Ignoring OptimisticLockException
Interview Cheat Sheet
Must know:
- Optimistic locking via @Version - automatically increments version on each UPDATE
- WHERE clause includes version check: WHERE id = ? AND version = ?
- On conflict (0 rows updated) -> OptimisticLockException
- LockModeType: OPTIMISTIC (version check), OPTIMISTIC_FORCE_INCREMENT (always increments)
- Retry pattern: @Retryable(value = OptimisticLockException.class, maxAttempts = 3)
- Version types: Integer, Long (auto-increment), Timestamp (risky - conflict in same millisecond)
Frequent follow-up questions:
- Why “optimistic”? Assumes no conflicts, locks only on write
- When OPTIMISTIC_FORCE_INCREMENT? On child collection change, for cache invalidation
- What’s better - optimistic vs pessimistic? Optimistic - high performance, rare conflicts; Pessimistic - frequent conflicts, critical data
- Timestamp vs Integer for @Version? Integer is more reliable - Timestamp may miss conflict if update happens in same millisecond
Red flags (DO NOT say):
- “Without @Version for financial data” - lost updates possible
- “Manual version change” - breaks locking mechanism
- “I ignore OptimisticLockException” - data silently lost
- “Pessimistic locking by default” - unnecessary locks, deadlocks possible
Related topics:
- [[18. How to Implement Pessimistic Locking in JPA]]
- [[19. What is @Version and Why is It Needed]]
- [[13. How Does the Flush Mechanism Work in Hibernate]]
- [[15. What Does the refresh() Method Do]]