A versioned JPA entity detects an update based on an older revision so one writer cannot silently overwrite a newer committed revision.
Spring JPA optimistic locking: reject a stale stock update
The downloadable Spring source kit pins Java 21 and Spring Boot 4.0.8 with its managed dependencies. Run mvn test to check the named fixture.
Construct two independent views
Two EntityManagers load the same stock account before either changes it. The first writer commits 90 available units. The second still holds the earlier revision and tries to write 70. The test expects OptimisticLockException at flush, then rolls back the failed transaction.
The entity has one Long version field marked Version. Application code changes available, not the version. This fixture performs an ordered two-view experiment; it reproduces a stale write without claiming to simulate every possible concurrency schedule or database isolation level.
A retry must repeat the decision
Catching the exception and resubmitting the old desired value can still discard the business intent behind a competing update. Reload current stock and recompute the reservation under the intended contract, with a bounded retry policy. A failed transaction’s persistence context should not be reused as though it were healthy.
Versioning one account does not protect an invariant spanning several unversioned rows. It also does not stop duplicate external side effects from a retry. Multi-object invariants and Transaction scope need explicit treatment before applying this to a real inventory service.
Checked source
try (var first = kit.factory.createEntityManager();
var second = kit.factory.createEntityManager()) {
var current = first.find(JpaContracts.StockAccount.class, 1L);
var stale = second.find(JpaContracts.StockAccount.class, 1L);
first.getTransaction().begin();
current.available = 90;
first.getTransaction().commit();
second.getTransaction().begin();
stale.available = 70;
assertThrows(OptimisticLockException.class, second::flush);
second.getTransaction().rollback();
}Test the boundary
JpaBoundaryTest.staleVersionRejectsSecondWriter checks this contract in the source kit. Excerpts belong to the named classes; use the downloadable files for imports, configuration and assertions.
Costs and boundaries
A version check adds revision data to the update predicate and failure handling to the application. Contention can increase retries and database work. The local test proves rejection for one stale account update; it does not prove multi-row serializability or quantify throughput.
Common Mistakes
- Do not edit the version manually.
- Roll back after a failed flush.
- Do not retry a stale decision without re-reading the state it depends on.
Read next
Spring JPA entity lifecycle: managed changes and detached objects, Spring transaction propagation: joined rollback and independent commit, Java JDBC isolation: uncommitted writes and separate sessions, Java multi-map invariants: one lock for a reservation boundary.
Continue with range and graph boundaries
Continue with Spring If-Match writes: reject a stale receipt revision.
