What is @Version and Why is It Needed
The @jakarta.persistence.Version annotation (formerly javax.persistence.Version) is the standard JPA specification annotation used to activate Optimistic Locking. Its primary ro...
The @jakarta.persistence.Version annotation (formerly javax.persistence.Version) is the standard JPA specification annotation used to activate Optimistic Locking. Its primary role is to protect relational data against the Lost Update concurrency anomaly, ensuring that concurrent transactions cannot silently overwrite each other’s changes without raising an explicit concurrency conflict.
🟢 Junior Level
Why @Version is Needed: Preventing Lost Updates
Consider a banking or flight reservation system operating without entity versioning:
- User A reads an account balance:
$100. - User B reads the same account balance concurrently:
$100. - User B withdraws
$20and saves the new balance:$80. - User A deposits
$50and commits their calculation ($100+$50=$150). - The Disaster: User A’s update completely overwrites User B’s withdrawal! The
$20deduction vanishes without a trace (the Lost Update anomaly).
The @Version annotation eliminates this risk at the ORM layer:
@Entity
@Table(name = "accounts")
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal balance;
@Version
private Long version; // Automatically managed and incremented by Hibernate
}
How Hibernate Manages the @Version Field
- On Initial Persist (
INSERT): Hibernate initializes theversioncolumn to0(or1depending on the dialect and version). - On Every Update (
UPDATE): Hibernate automatically increments the version by 1 and includes a verification check in theWHEREclause:UPDATE accounts SET balance = ?, version = version + 1 WHERE id = ? AND version = ?; - On Version Collision: If another transaction already incremented the version in the database, the SQL statement updates
0rows. Hibernate detects this and immediately throwsjakarta.persistence.OptimisticLockException.
🟡 Middle Level
Supported Data Types for @Version
The JPA specification strictly defines which Java data types are permitted on properties annotated with @Version:
| Java Type | SQL Column Type | Concurrency Behavior | Recommendation |
|---|---|---|---|
long / Long |
BIGINT |
Monotonically increments ($0, 1, 2…$). Practically impossible to overflow ($2^{63}-1$). | 🏆 Best Practice Standard |
int / Integer |
INTEGER |
Monotonically increments. Suitable for standard entity lifecycles (up to 2 billion revisions). | ✅ Fully Recommended |
short / Short |
SMALLINT |
Limited to 32,767 revisions. Risks overflow exceptions on heavily updated entities. | ⚠️ Avoid for hot records |
java.sql.Timestamp / Instant |
TIMESTAMP |
Populates the database or system clock timestamp on each update. | ❌ Dangerous in Production (millisecond collision risks) |
The Critical Difference Between @Version and @LastModifiedDate
Developers often confuse optimistic versioning with auditing timestamps:
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Document {
@Id
private Long id;
// 1. Used strictly for Optimistic Locking and transaction isolation
@Version
private Long version;
// 2. Used strictly for business auditing, analytics, and UI display
@LastModifiedDate
private Instant updatedAt;
}
@Version: Reserved strictly for the ORM engine and database concurrency control. Mutating this field manually in business code is strictly forbidden (document.setVersion(5)will corrupt Hibernate’s change-tracking).@LastModifiedDate: Managed by Spring Data Auditing. It does not participate in optimistic lockingWHERE ... AND version = ?predicates.
🔴 Senior Level
Senior Impact: How @Version Governs “New Entity” Detection in Spring Data JPA
When entities use manually assigned primary keys (such as natural business codes, tax numbers, or client-generated UUIDs), a classic Spring Data performance issue arises:
- Spring Data’s
SimpleJpaRepository.save()checksentityInformation.isNew(entity). - If an entity has no
@Versionfield, Spring checks ifid == null. Because the assigned ID is already set before persisting,isNew()returnsfalse, causing Spring to callentityManager.merge(). This triggers an unwanted penaltySELECTbefore everyINSERT! - The Senior Insight: If the entity declares a
@Versionfield using an object wrapper (LongorInteger), Spring Data alters its new-entity detection strategy:// Inside AbstractEntityInformation: if (versionAttribute != null) { return versionAttribute.get(entity) == null; // If version == null, entity is NEW! }Because newly instantiated objects have
version == null, Spring Data accurately identifies the entity as new and invokes the fast, directentityManager.persist()without issuing any penaltySELECTqueries!
NEW ENTITY RESOLUTION LOGIC IN SPRING DATA JPA
[ save(entity) ]
│
Is @Version declared on Entity?
/ \
YES / \ NO
/ \
Is version == null? Is id == null?
/ \ / \
YES / \ NO YES / \ NO
▼ ▼ ▼ ▼
persist() merge() persist() merge()
(No SELECT) (With SELECT!) (No SELECT) (With SELECT!)
@Version and Bulk Operations (@Modifying Queries)
A notorious trap for senior backend engineers involves mass DML updates:
// Mass price adjustment via JPQL:
@Modifying
@Query("UPDATE Product p SET p.price = p.price * 1.1 WHERE p.category = :category")
int increaseCategoryPrices(@Param("category") String category);
Internal Consequences:
- Bulk
UPDATEandDELETEqueries translate directly into a single SQL statement sent to the database engine. - Hibernate’s Dirty Checking and entity lifecycle event listeners are completely bypassed.
- Hibernate DOES NOT automatically increment the
versioncolumn! - Architectural Failure: The
versioncolumn in the database remains stale. If any of those entities reside in L1/L2 caches, the application will operate on inconsistent, out-of-sync version states. - The Mandated Rule: Whenever executing
@Modifyingqueries on versioned entities, you must explicitly increment the version column inside the query text:@Modifying @Query("UPDATE Product p SET p.price = p.price * 1.1, p.version = p.version + 1 WHERE p.category = :category") int increaseCategoryPricesWithVersionIncrement(@Param("category") String category);
4 Tricky Questions
1. Can you declare two distinct @Version fields in a single JPA entity class (e.g., a numeric version for locking and a timestamp version for time tracking)?
Answer:
- Categorically NO. The JPA specification (Section 4.4.4.2 Optimistic Locking) explicitly forbids declaring more than one
@Versionfield per entity hierarchy. - Attempting to bootstrap an application with multiple
@Versionannotations will fail during metamodel initialization with:org.hibernate.AnnotationException: A factory cannot support multiple version properties: com.example.Account. - For tracking modification timestamps, use Spring Data’s
@LastModifiedDateor Hibernate’s@UpdateTimestampwithout@Version.
2. What happens if a detached entity is passed to entityManager.merge(), but the client modified or nulled out the version property (version = null) before submission?
Answer:
- If
version == null: Hibernate misinterprets the entity as new and attempts an SQLINSERT. If the primary key is already taken, the database engine throws a Unique Key Constraint violation. - If the
versionwas altered to an invalid value (e.g., client sendsversion = 1, but the database holdsversion = 2): themerge()method executes an initialSELECT, detects the version mismatch between the detached object and the database row, and immediately throws anOptimisticLockExceptionbefore executing any mutations.
3. Does Hibernate increment the entity’s version if the transaction performs only read operations without calling setters, but uses LockModeType.OPTIMISTIC?
Answer:
- No. Under
LockModeType.OPTIMISTIC(formerlyREAD), Hibernate does not increment the version. - During the pre-commit
flush()phase, Hibernate executes an extra verification query:SELECT version FROM table WHERE id = ?. - If the database version still matches the version read at the start of the transaction, the commit succeeds and the version remains unchanged.
- To force an increment on read without mutating scalar fields, you must explicitly specify
LockModeType.OPTIMISTIC_FORCE_INCREMENT.
4. Why doesn’t the @Version field prevent duplicate processing of the same incoming event from Kafka in a microservices cluster if idempotency is missing?
Answer:
- If two microservice instances consume duplicate creation events concurrently, both entities initially possess
version == null(or0on insert). - Optimistic locking guards only SQL
UPDATEoperations via theWHERE version = ?predicate. - During concurrent
INSERToperations, both transactions attempt to insert a new row with the same business key. The@Versionfield provides zero protection against duplicate inserts; only a database-levelUNIQUEconstraint on the business key can prevent duplicate records.
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“The
@Versionannotation in JPA activates Optimistic Locking to eliminate the Lost Update anomaly without physical database row locks. It should be placed on a numeric field (LongorInteger). On everyUPDATE, Hibernate increments the version and validatesWHERE id = ? AND version = ?. If another transaction modified the row first, 0 rows are updated and Hibernate throwsOptimisticLockException. In Spring Data JPA, an object-typed@Version(Long) also solves the assigned ID problem: ifversion == null, Spring recognizes the entity as new and calls fastpersist()instead ofmerge(). For bulk@Modifyingqueries, the version must be incremented manually in the JPQL query.”
Key Production Metrics & Health Checks
- OptimisticLockException Frequency: Identifies hot entities experiencing high write contention across microservice instances.
- Entity Revision Ceilings: Abnormally high version numbers on individual rows indicate hot-spot entities undergoing excessive write churn.
Red Flags to Avoid
- ❌ “I use Timestamp for
@Versionbecause it tells me when the record was updated.” — High-concurrency millisecond collisions make timestamps unreliable; always useLongorInteger. - ❌ “I write
entity.setVersion(v + 1)in business service logic.” — Application code must never manually alter version numbers. - ❌ “Bulk
@Modifyingupdate queries automatically increment@Version.” — Bulk queries bypass the persistence context; you must increment the version explicitly in JPQL. - ❌ “An entity can have two
@Versionannotations.” — Prohibited by JPA; fails at bootstrap withAnnotationException.
Related Topics
- How to Implement Optimistic Locking in JPA — Implementation patterns
- How to Implement Pessimistic Locking in JPA — Locking comparisons
- How Does the Flush Mechanism Work in Hibernate — Flush and updates
- What Does the refresh() Method Do — Version synchronization
- What is the Difference Between persist() and merge() — New entity detection