skip to content

@Lock & Optimistic/Pessimistic Locking

@Lock puts an optimistic or pessimistic lock mode on a repository method, working with a @Version field for the optimistic case. Interviewers use a concurrent-update scenario and expect you to pick a strategy and defend the retry behaviour.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

What is optimistic locking in JPA, and how does the @Version field implement it?

level: juniorimportance: must knowfreq 60%

answer

  1. version column in UPDATE ... WHERE id=? AND version=?
  2. 0 rows affected -> OptimisticLockException
  3. Spring -> ObjectOptimisticLockingFailureException
  4. no DB lock, detect at flush
  5. @Version numeric or timestamp, provider-managed

basics

~10 s

Optimistic locking detects concurrent edits without locking rows. You add a @Version field; Hibernate checks it in the UPDATE's WHERE clause and throws an error if another transaction already changed the row.

solid answer

~40 s

Optimistic locking assumes conflicts are rare, so it takes no database lock. You annotate a numeric or timestamp field with jakarta.persistence.@Version. When an entity is updated, Hibernate emits UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?. If another transaction already committed a change, the version no longer matches, zero rows update, and Hibernate throws OptimisticLockException — which Spring Data translates to ObjectOptimisticLockingFailureException. This lets you catch the conflict and retry or surface it to the user. It costs one extra column and no lock contention, so it scales well for low-conflict, read-heavy workloads. The version field is managed entirely by the provider; you never set it manually.

code

java · 15 lines
java
@Entity
public class Account {
    @Id
    private Long id;

    @Version                 // Hibernate manages this; never set it manually
    private Long version;

    private BigDecimal balance;
}

// On save() Hibernate runs:
//   UPDATE account SET balance=?, version=? WHERE id=? AND version=?
// If 0 rows update -> OptimisticLockException
//   -> Spring ObjectOptimisticLockingFailureException

go deeper

for a junior

Know that @Version adds a column checked in the UPDATE WHERE clause and an exception is thrown on conflict.

for a middle

Explain the exact SQL, the 0-rows-affected mechanic, and the Spring exception translation.

for a senior

Discuss detached-entity/merge conflict detection, allowed version types, and read-heavy scaling rationale.

for a principal

Frame optimistic vs pessimistic as an architectural default; tie version bumps to aggregate consistency boundaries.

## What problem it solves When two transactions read the same row and both write it back, the second write can silently overwrite the first — the *lost update* problem. **Optimistic locking** prevents this without holding any database lock: it *assumes* conflicts are rare and only *detects* them at write time. ## The @Version field You add a field annotated with `jakarta.persistence.@Version` (older code: `javax.persistence.@Version`): ```java @Entity class Account { @Id Long id; @Version Long version; // managed by Hibernate, never set by you BigDecimal balance; } ``` Allowed types: `int`/`Integer`, `long`/`Long`, `short`/`Short`, `java.sql.Timestamp`, or `java.time.Instant`/`LocalDateTime` (Hibernate). Numeric versions are preferred — timestamps have resolution problems under high concurrency. ## How the check works On every flush of a *dirty* entity, Hibernate generates: ```sql UPDATE account SET balance = ?, version = ? WHERE id = ? AND version = ? ``` The `version` in the SET is the current value + 1; the `version` in the WHERE is the value that was loaded. The JDBC driver returns the **row count**. If it is `1`, the update succeeded. If it is `0`, some other transaction already committed a newer version, so the WHERE matched nothing → Hibernate throws `org.hibernate.StaleObjectStateException`, wrapped as JPA `jakarta.persistence.OptimisticLockException`. ## Spring's exception translation Because Spring Data repositories sit behind `PersistenceExceptionTranslationPostProcessor`, the provider exception is translated into Spring's `org.springframework.orm.ObjectOptimisticLockingFailureException` (a subclass of `DataAccessException`). Catch that in your service to retry or report a conflict. ## When it fires - Only when the entity is actually **modified** (dirty) and flushed, *unless* you explicitly request an `OPTIMISTIC` lock (which forces a version check even for read-only entities). - The check happens at **flush/commit**, not at read time. Two transactions can both read version 5; the first to commit wins, the second gets the exception. ## Gotchas - **You must not set `version` yourself** — the provider owns it. Manually mutating it breaks the mechanism. - **Detached-entity merges**: if you send an entity to a web client and merge it back, include the version so stale submits are caught (this is how you get browser-tab-level conflict detection). - **No `@Version` field = no optimistic locking**, and requesting `LockModeType.OPTIMISTIC` on a repository method for such an entity throws an exception. - The version column should have a NOT NULL default handled by JPA; a null version on a new entity is fine (it starts at 0/1). ## When to use Default choice for most web CRUD: low write-conflict rates, no lock held across user think-time, horizontally scalable. Switch to pessimistic locking only when conflicts are frequent or a business invariant must be serialized at the DB.

  • At what point does the OptimisticLockException actually get thrown?
    At flush/commit — when Hibernate emits the versioned UPDATE and the driver reports 0 rows affected. Not at read time. That's why two transactions can both read version 5 and only the second committer fails.
  • Which Spring exception wraps the JPA OptimisticLockException, and why does that matter?
    org.springframework.orm.ObjectOptimisticLockingFailureException, a DataAccessException subclass produced by Spring's persistence-exception translation. It matters because you catch the Spring type in your service layer to implement retry logic, staying decoupled from the JPA/Hibernate API.

saying these in an interview costs you the question

  • Thinking @Version takes a database lock (it doesn't — no lock is held).
  • Saying the conflict is detected at read time rather than at flush/commit.
  • Claiming you must increment the version field manually in code.
  • Believing optimistic locking blocks other transactions from writing.

context

open as a page

How do you request a pessimistic lock on a Spring Data repository method, and what's the difference between PESSIMISTIC_READ and PESSIMISTIC_WRITE?

level: middleimportance: should knowfreq 45%

basics

~10 s

Put @Lock(LockModeType.PESSIMISTIC_WRITE) on the repository query method. PESSIMISTIC_READ is a shared lock (others can read, not write); PESSIMISTIC_WRITE is an exclusive lock (SELECT ... FOR UPDATE) that blocks other writers and lockers.

open as a page

What do LockModeType.OPTIMISTIC and OPTIMISTIC_FORCE_INCREMENT do, and how do they differ from plain @Version behavior?

level: seniorimportance: should knowfreq 30%

basics

~20 s

OPTIMISTIC forces a version check at commit even if you only read the entity (guarding against concurrent changes). OPTIMISTIC_FORCE_INCREMENT additionally bumps the entity's @Version even when the entity itself wasn't modified — useful to signal an aggregate changed.

open as a page

What operational risks come with pessimistic locking (timeouts, deadlocks, held locks), and how do you mitigate them in Spring Data JPA?

level: seniorimportance: should knowfreq 32%

basics

~10 s

Pessimistic locks block other transactions, so you risk long waits, deadlocks, and throughput loss. Mitigate with a jakarta.persistence.lock.timeout query hint, consistent lock ordering, short transactions, and never holding a lock across a user round-trip.

open as a page

How do you decide between optimistic and pessimistic locking for a given operation, and how do you handle the resulting failures gracefully?

level: principalimportance: should knowfreq 26%

basics

~20 s

Use optimistic (@Version) by default — no locks, scales, retry rare conflicts. Use pessimistic (@Lock FOR UPDATE) only for hot, high-contention rows where retries would storm. Handle failures by catching the specific Spring exception and retrying with backoff or reporting a conflict.

open as a page