Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Spring JPA optimistic locking: reject a stale stock update

Last updated: 30 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

A versioned JPA entity detects an update based on an older revision so one writer cannot silently overwrite a newer committed revision.

Download Spring source kit

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

Java
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.

spring
spring-boot
jpa-optimistic-locking
Storage details