Interview Prep Zoneby CuriouserLabs

Question 5 of 5 · staff level

Optimistic vs pessimistic locking with JPA — how does @Version work, and how do you choose?

🎤 Say this first

Optimistic (@Version): every UPDATE carries WHERE version = ?; a concurrent modification makes the update match zero rows → OptimisticLockingFailureException → caller retries or reports conflict. No locks held, scales for low-contention, works across detached/user-think-time edits — the default for web apps. Pessimistic (SELECT … FOR UPDATE via @Lock(PESSIMISTIC_WRITE)): block others up front — for hot rows where retries would thrash (inventory decrements, account balances, queue-like tables). Choose by conflict probability × cost of retry; and remember the third option — don't lock, make the write atomic (UPDATE stock SET qty = qty - 1 WHERE qty >= 1).

The full picture

Both modes in practice
@Entity class Product {
    @Id Long id;
    @Version long version;          // JPA increments; UPDATE … WHERE version = :v
    int stock;
}

// Optimistic conflict surfaces as a 409 to the client:
@Transactional
public void rename(long id, String name, long expectedVersion) {
    var p = repo.findById(id).orElseThrow();
    if (p.getVersion() != expectedVersion)       // stale form data
        throw new ConflictException();           // client refetches & retries
    p.setName(name);
}

// Pessimistic for the hot path:
public interface ProductRepo extends JpaRepository<Product, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @QueryHints(@QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000"))
    Optional<Product> findWithLockById(Long id);   // SELECT … FOR UPDATE
}
  • Optimistic retry belongs in application middleware (@Retryable on OptimisticLockingFailureException, capped attempts + jitter) — but only when the operation is idempotent to recompute.
  • Pessimistic lock scope = transaction scope: acquiring FOR UPDATE then calling a slow remote API serializes all competitors behind your latency. Locks + short transactions, always.
  • Lock ordering discipline applies in SQL too: two services locking rows in opposite orders = DB deadlock; the DB kills one (translated to CannotAcquireLockException/DeadlockLoserDataAccessException — retryable).
  • For user-facing edits, @Version doubles as the ETag/If-Match value — optimistic concurrency end-to-end through HTTP.
  • Cross-service, neither works: you can't hold DB locks across service boundaries — that's reservation patterns / sagas territory.

🔄 Likely follow-up questions

  • OPTIMISTIC_FORCE_INCREMENT — what problem does it solve for aggregates?
  • How do lock timeouts differ across Postgres/MySQL/Oracle, and what does JPA guarantee?
  • Design inventory reservation for a flash sale — locks, atomic updates, or queues?