Optimistic locking with @Version

Hibernate · lesson 14 of 32 · 4 min read

Stop concurrent updates from silently overwriting each other, without holding row locks.

Open this lesson in the learning hub

Key points

  • Two users load the same row and both save. Unprotected, the second write erases the first without a sound - a lost update.
  • Add a @Version field (long, int or Instant). Hibernate increments it on every flush; never set it yourself.
  • Every UPDATE becomes ... where id = ? and version = ?. Zero rows affected means somebody else got there first.
  • You then get an OptimisticLockException, wrapped by Spring as ObjectOptimisticLockingFailureException. Reload, retry, or tell the user.
  • Optimistic means no lock is held, so it scales. Use LockModeType.PESSIMISTIC_WRITE only for short, genuinely hot contention.
  • Send the version to the browser and back with the form. That also catches edits made while the page sat open.

Example

@Entity
public class Account {

    @Id @GeneratedValue private Long id;
    private long balanceCents;

    @Version
    private long version;      // Hibernate bumps this on every flush

    public void withdraw(long cents) {
        if (cents > balanceCents) throw new InsufficientFundsException();
        balanceCents -= cents;
    }
}

@Service
public class TransferService {

    @Retryable(retryFor = ObjectOptimisticLockingFailureException.class, maxAttempts = 3)
    @Transactional
    public void withdraw(Long accountId, long cents) {
        em.find(Account.class, accountId).withdraw(cents);
        // update account set balance_cents=?, version=? where id=? and version=?
    }
}

One extra column turns a silent lost update into a loud, handleable failure.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Hibernate course, and every lesson in it is listed on the Hibernate contents page.