skip to content

What does JPA's LockModeType.PESSIMISTIC_FORCE_INCREMENT do that LockModeType.PESSIMISTIC_WRITE does not, and when would you reach for it?

level: seniorimportance: should knowfreq 30%

answer

  1. exclusive lock + immediate version bump
  2. bump at lock time, not at flush
  3. requires a version attribute
  4. parent lock to protect a child-row invariant
  5. turns readers into writers on a hot row

basics

~20 s

It takes the exclusive row lock and also bumps the entity's version column immediately, in the same operation, even if you never modify the entity. Use it to invalidate other transactions' copies of a parent whose children you are changing. The entity must have a version attribute.

solid answer

~50 s

`PESSIMISTIC_WRITE` locks the row; `PESSIMISTIC_FORCE_INCREMENT` locks it **and** increments the version attribute right away — Hibernate emits the `for update` select and then an `update ... set version = version + 1 where id = ? and version = ?`. The bump happens at lock time, not at flush, and it happens even if the entity's own columns never change. The entity must be versioned; there is nothing to increment otherwise and Hibernate raises an error rather than silently degrading. The canonical use is aggregate-level consistency. Suppose invariant "an order's lines must sum to at most the order's credit limit" spans an `Order` and its `OrderLine` rows. Inserting a line changes nothing on the `orders` row, so any other transaction holding that `Order` would not notice. Locking the parent with `PESSIMISTIC_FORCE_INCREMENT` serialises writers on the parent *and* moves its version forward, so anyone else who read that parent will fail their own version check.

code

sql · 2 lines
sql
select o.id, o.version, o.credit_limit from orders o where o.id = ? for update;
update orders set version = ? where id = ? and version = ?;

go deeper

for a junior

Know it locks the row and also increments the version immediately, and that a version attribute is required.

for a middle

Add the two-statement SQL (FOR UPDATE select, then a versioned UPDATE) and that the bump happens at lock time regardless of modifications.

for a senior

Motivate it with the cross-row aggregate invariant — parent version bumped because a child changed — and discuss the contention it creates on the parent row.

for a principal

Frame it as encoding aggregate boundaries in the locking strategy, and weigh it against redesigning the aggregate or pushing the invariant into a database constraint.

## Two effects in one mode A plain exclusive lock protects a row only for the lifetime of your transaction. The instant you commit, the lock is gone and there is no trace that anything happened — if the row's own columns did not change, other transactions that read it earlier still believe their copy is current. `PESSIMISTIC_FORCE_INCREMENT` adds that trace. It performs the exclusive lock and then increments the entity's version attribute immediately, in the same operation, whether or not you dirty a single field. The generated SQL is two statements: `select ... where id = ? for update`, then `update ... set version = ? where id = ? and version = ?`. Because the increment is real, committed data, its effect outlives your lock: anyone else who loaded that entity before you now has a stale version and will fail on their next write. The increment happens at **lock time**, not at flush time. That is deliberate — the version bump is part of taking the lock, so ordering is predictable and the lock is not silently a no-op for a transaction that only reads. ## Why the version column is mandatory The mode is defined in terms of a version attribute. Applied to an entity without one there is nothing to increment, and the guarantee the mode promises cannot be delivered, so Hibernate raises an error instead of quietly behaving like `PESSIMISTIC_WRITE`. If you need this behaviour on a table you cannot alter, the mode is simply unavailable. ## The aggregate problem it solves The real motivation is invariants that span more than one row. Consider `Post` and `PostComment`, with a rule that a post may not exceed N comments, or `Order` and `OrderLine` with a total cap. Adding a child row touches only the child table. Two transactions can each read the parent, each add a child, and each commit — the parent's version never moves, no per-row lock ever conflicts, and the invariant quietly breaks. Locking the *parent* with `PESSIMISTIC_FORCE_INCREMENT` fixes both halves of that. The exclusive lock serialises everyone trying to modify the aggregate through the parent, so the invariant can be checked safely. The version bump propagates the change outwards: any other transaction or any user session that read the parent earlier now finds its version outdated, which is exactly the semantics you want — the aggregate has changed, even if the parent's own columns did not. ## When not to use it If you are going to modify the entity's own fields anyway, `PESSIMISTIC_WRITE` is enough: the flush will bump the version for you as part of the normal versioned update. Reach for the force-increment mode only when the change lives elsewhere but must be *attributed* to this entity. Be aware of the cost. Every reader that takes this mode becomes a writer: it issues an `update` and creates contention on the parent row, which is precisely the row everyone in the aggregate touches. On a hot aggregate that turns concurrent child inserts into a fully serialised queue. That may be exactly the trade you want for correctness, but it should be a decision, not an accident — the alternative designs are narrowing the aggregate so the invariant no longer spans rows, or enforcing it with a database constraint or a dedicated counter row. Finally, note this is a *pessimistic* mode: unlike its optimistic counterpart it takes a real row lock, so it blocks rather than failing at commit, and everything about lock duration, timeouts and pool pressure applies to it.

  • Why bump a version when you are not changing the entity at all?
    Because the change lives in rows the entity owns — a new child row, for example — and there would otherwise be no evidence on the parent that the aggregate moved. Incrementing the parent's version makes every other transaction that had read that parent fail its next write, which is the correct outcome when the aggregate's state has changed underneath it.
  • What happens if you request this mode on an entity with no version attribute?
    It fails: there is no version to increment, so the guarantee cannot be provided and Hibernate raises an error rather than degrading to a plain exclusive lock. If the schema cannot get a version column, you need a different strategy — narrowing the aggregate, a database constraint, or an explicit counter row you lock and update yourself.

saying these in an interview costs you the question

  • Thinking the version increment is deferred to flush or only applied if the entity was modified.
  • Using it on an entity you are already modifying, where the ordinary versioned update would bump the version anyway.
  • Assuming it works without a version attribute and silently behaves like PESSIMISTIC_WRITE.
  • Ignoring that it makes every such reader a writer on the parent row, serialising the whole aggregate.

context