Optimistic Lock Conflict Handling

Optimistic locking detects concurrent updates by requiring the entity version read by the application to still match the version stored in the database. When another transaction has changed the row first, the update affects no row or the persistence layer raises an optimistic-lock exception, which the application must handle explicitly.

Optimistic locking assumes conflicts are relatively uncommon, so transactions do not hold database locks while an entity is being read and edited. Instead, an entity carries a version value, commonly through JPA's `@Version` annotation, and the persistence provider uses that value when updating the row.

A typical update effectively checks both the entity identifier and its expected version: `UPDATE ... SET ..., version = version + 1 WHERE id = ? AND version = ?`. If another transaction has already updated the row, the stored version is different, so the update matches zero rows. JPA/Hibernate can surface this as an `OptimisticLockException` or a related persistence exception depending on the operation and context.

Conflict handling is a business and transaction-management concern, not merely an exception-catching exercise. For some operations, the correct response is to reload the current entity and ask the user to reconcile changes; for others, a bounded retry can be appropriate when the operation is safe to recompute. Blindly retrying can overwrite intended changes or repeatedly reproduce the conflict.

The transaction boundary matters. A retry should normally start a fresh transaction because the persistence context involved in the failed transaction may contain stale state or be marked for rollback. In Spring applications, this often means placing the retried operation behind a transaction boundary rather than simply looping inside one failed transaction.

Key points

Interview questions

How does optimistic locking prevent a stale update from overwriting a newer update?

The entity contains a version value that represents the state observed by the application. The update includes that expected version in its `WHERE` condition and normally increments the version when the update succeeds. If another transaction has already changed the row, the expected version no longer matches, so the stale update affects no row and the persistence layer reports a conflict. The application can then reload current state and decide how to proceed.

What is the difference between optimistic and pessimistic locking?

Optimistic locking allows concurrent work and detects conflicts when an update is attempted. Pessimistic locking instead obtains a database lock so conflicting transactions are restricted while the lock is held. Optimistic locking is often useful when collisions are uncommon and waiting on locks would be unnecessary. Pessimistic locking can be appropriate when conflicts are frequent or the application requires stronger serialization during a critical section.

You catch an `OptimisticLockException`. What should you do next?

First, treat the transaction as failed rather than continuing with the same stale persistence context. Start a fresh transaction and reload the current entity if the business operation is retryable. Recompute the intended change against that current state, and retry only a bounded number of times when doing so is safe. If the operation represents a user edit, it may be better to report the conflict and let the user reconcile the changes.

Why can simply adding `@Version` fail to solve an application's concurrency problem?

`@Version` protects the versioned entity update, but it does not automatically define what the application should do after a conflict. A conflict may require merging changes, rejecting the user's edit, or safely retrying a recalculated command. Transaction boundaries, detached entities, bulk updates, and operations that bypass normal entity versioning can also affect the protection provided. Therefore, optimistic locking is one part of a concurrency strategy rather than a complete conflict-resolution policy.

How would you design an HTTP API that exposes optimistic-lock conflicts?

The API should carry enough concurrency information for the server to determine whether the client's representation is current, such as a version or an HTTP entity tag. A stale write should produce a response that clearly indicates a concurrency conflict rather than silently replacing newer data. The client can then fetch current state and either merge or ask the user to resolve the difference. The exact HTTP status and representation should follow the API's established contract, but the key property is that stale data is not silently accepted.