How do you request a pessimistic lock on a Spring Data repository method, and what's the difference between PESSIMISTIC_READ and PESSIMISTIC_WRITE?
answer
- @Lock on repository method, LockModeType arg
- PESSIMISTIC_READ = FOR SHARE (others read, can't write)
- PESSIMISTIC_WRITE = FOR UPDATE (exclusive)
- needs @Transactional, lock held till commit
- lock.timeout query hint to bound waiting
basics
~10 sPut @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.
solid answer
~40 sAnnotate a Spring Data repository query method with org.springframework.data.jpa.repository.@Lock, passing a jakarta.persistence.LockModeType. PESSIMISTIC_READ acquires a *shared* lock — typically SELECT ... FOR SHARE — letting other transactions read the row but preventing them from updating it until you commit. PESSIMISTIC_WRITE acquires an *exclusive* lock — SELECT ... FOR UPDATE — blocking other transactions from reading-for-update or writing the row until your transaction ends. Both take a real database lock held for the duration of the transaction, so the method must run inside @Transactional. Use PESSIMISTIC_WRITE for read-modify-write on hot rows (inventory counters, wallet balances) where you can't tolerate retry storms. The exact SQL depends on the dialect. You typically pair @Lock with a lock timeout via @QueryHints to avoid indefinite blocking.
code
java · 17 linesinterface AccountRepository extends JpaRepository<Account, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE) // SELECT ... FOR UPDATE
@QueryHints({ @QueryHint(
name = "jakarta.persistence.lock.timeout", value = "3000") })
@Query("select a from Account a where a.id = :id")
Optional<Account> findByIdForUpdate(@Param("id") Long id);
}
@Service
class TransferService {
@Transactional // lock held for this tx
public void withdraw(Long id, BigDecimal amount) {
Account a = repo.findByIdForUpdate(id).orElseThrow();
a.setBalance(a.getBalance().subtract(amount)); // safe read-modify-write
}
}go deeper
Know @Lock exists and PESSIMISTIC_WRITE means SELECT FOR UPDATE.
Distinguish shared vs exclusive locks, the @Transactional requirement, and lock timeouts.
Reason about deadlock ordering, MVCC read-committed interaction, and keeping transactions short.
Set policy on when contention justifies pessimistic locking vs optimistic retry, and cap lock duration to protect throughput.
## Requesting the lock Spring Data JPA exposes locking through `org.springframework.data.jpa.repository.@Lock` placed on a repository method: ```java interface AccountRepository extends JpaRepository<Account, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select a from Account a where a.id = :id") Optional<Account> findByIdForUpdate(@Param("id") Long id); } ``` You can also annotate a derived query method (e.g. override `findById`). The `LockModeType` enum lives in `jakarta.persistence`. ## Pessimistic vs optimistic **Pessimistic locking** *assumes* conflicts are likely and takes a real database lock *up front* so no one else can interfere. It trades throughput (blocking) for guaranteed serialization. Optimistic locking takes no lock and detects conflicts after the fact. ## PESSIMISTIC_READ (shared lock) - Emits a *shared* lock — on PostgreSQL `SELECT ... FOR SHARE`, on MySQL/InnoDB `... LOCK IN SHARE MODE` (or `FOR SHARE`). - **Other transactions may still read** the row (including acquiring their own shared lock), but **cannot update or delete** it, and cannot get a PESSIMISTIC_WRITE lock, until you commit. - Use when you need the row to stay unchanged while you read related data, but you don't necessarily intend to write it. ## PESSIMISTIC_WRITE (exclusive lock) - Emits `SELECT ... FOR UPDATE`. - **Blocks other transactions** from acquiring any pessimistic lock (read or write) and from updating/deleting the row until you commit or roll back. Plain non-locking reads may still see the last committed snapshot under MVCC databases (read-committed), but *locking* reads and writes queue behind you. - The classic **read-modify-write** guard for hot rows. ## Transaction requirement A pessimistic lock is held **for the lifetime of the transaction**. Therefore the repository call must run inside an active transaction — annotate the calling service method with `@Transactional`. Calling a locking method with no transaction either fails or silently acquires-and-immediately-releases the lock (implementation dependent), which defeats the purpose. The lock is released at commit/rollback; you cannot manually unlock earlier. ## Lock timeout By default a blocked transaction may wait indefinitely (or until a DB deadlock detector fires). Bound the wait with a query hint: ```java @Lock(LockModeType.PESSIMISTIC_WRITE) @QueryHints({ @QueryHint(name = "jakarta.persistence.lock.timeout", value = "3000") }) Optional<Account> findByIdForUpdate(@Param("id") Long id); ``` The value is milliseconds. Support and semantics vary by database (PostgreSQL maps it to `NOWAIT`/`SKIP LOCKED` behavior only at specific values; many setups honor `0` = NOWAIT). On timeout you get `LockTimeoutException` → Spring `PessimisticLockingFailureException`/`CannotAcquireLockException`. ## Gotchas - **Deadlocks**: two transactions locking rows A then B vs B then A deadlock. The DB kills one victim → `CannotAcquireLockException`. Always lock rows in a consistent order. - **Lock held across user think-time is an anti-pattern** — never span an HTTP round-trip with a pessimistic lock; keep the transaction short. - **Requires DB support** for the locking SQL; some in-memory or exotic stores ignore it. - **PESSIMISTIC_FORCE_INCREMENT** additionally bumps the `@Version` column while holding the write lock. ## When to use which - Frequent, contended read-modify-write on a single row → `PESSIMISTIC_WRITE`. - You must prevent updates while you compute, but tolerate concurrent readers → `PESSIMISTIC_READ`. - Rare conflicts, read-heavy, scalable → prefer optimistic `@Version` instead.
- Why must the locking repository method run inside @Transactional?A pessimistic lock is held until the transaction ends. Without an enclosing transaction there is no boundary to hold it across — the lock would be released immediately (or the call fails), so the read-modify-write is no longer protected. @Transactional defines the lock's lifetime.
- How do you keep a blocked transaction from waiting forever on a locked row?Add a lock timeout via @QueryHints with hint name jakarta.persistence.lock.timeout (milliseconds). On expiry Hibernate throws LockTimeoutException, translated to Spring's PessimisticLockingFailureException / CannotAcquireLockException, which you can retry or fail fast. DB support for the exact value varies.
saying these in an interview costs you the question
- Saying PESSIMISTIC_READ blocks other transactions from reading the row (it only blocks writes).
- Applying @Lock without any surrounding @Transactional.
- Claiming the lock is released as soon as the repository method returns rather than at transaction end.
- Confusing @Lock (repository, LockModeType) with @Version (entity field).