Question 17 · Section 16

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.

Language versions: English Russian Ukrainian

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]]