💾 Section 16 · Question #19

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:

  1. User A reads an account balance: $100.
  2. User B reads the same account balance concurrently: $100.
  3. User B withdraws $20 and saves the new balance: $80.
  4. User A deposits $50 and commits their calculation ($100 + $50 = $150).
  5. The Disaster: User A’s update completely overwrites User B’s withdrawal! The $20 deduction 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 the version column to 0 (or 1 depending on the dialect and version).
  • On Every Update (UPDATE): Hibernate automatically increments the version by 1 and includes a verification check in the WHERE clause:
    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 0 rows. Hibernate detects this and immediately throws jakarta.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 locking WHERE ... 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() checks entityInformation.isNew(entity).
  • If an entity has no @Version field, Spring checks if id == null. Because the assigned ID is already set before persisting, isNew() returns false, causing Spring to call entityManager.merge(). This triggers an unwanted penalty SELECT before every INSERT!
  • The Senior Insight: If the entity declares a @Version field using an object wrapper (Long or Integer), 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, direct entityManager.persist() without issuing any penalty SELECT queries!

               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:

  1. Bulk UPDATE and DELETE queries translate directly into a single SQL statement sent to the database engine.
  2. Hibernate’s Dirty Checking and entity lifecycle event listeners are completely bypassed.
  3. Hibernate DOES NOT automatically increment the version column!
  4. Architectural Failure: The version column 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.
  5. The Mandated Rule: Whenever executing @Modifying queries 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:

  1. Categorically NO. The JPA specification (Section 4.4.4.2 Optimistic Locking) explicitly forbids declaring more than one @Version field per entity hierarchy.
  2. Attempting to bootstrap an application with multiple @Version annotations will fail during metamodel initialization with: org.hibernate.AnnotationException: A factory cannot support multiple version properties: com.example.Account.
  3. For tracking modification timestamps, use Spring Data’s @LastModifiedDate or Hibernate’s @UpdateTimestamp without @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:

  1. If version == null: Hibernate misinterprets the entity as new and attempts an SQL INSERT. If the primary key is already taken, the database engine throws a Unique Key Constraint violation.
  2. If the version was altered to an invalid value (e.g., client sends version = 1, but the database holds version = 2): the merge() method executes an initial SELECT, detects the version mismatch between the detached object and the database row, and immediately throws an OptimisticLockException before 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:

  1. No. Under LockModeType.OPTIMISTIC (formerly READ), Hibernate does not increment the version.
  2. During the pre-commit flush() phase, Hibernate executes an extra verification query: SELECT version FROM table WHERE id = ?.
  3. If the database version still matches the version read at the start of the transaction, the commit succeeds and the version remains unchanged.
  4. 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:

  1. If two microservice instances consume duplicate creation events concurrently, both entities initially possess version == null (or 0 on insert).
  2. Optimistic locking guards only SQL UPDATE operations via the WHERE version = ? predicate.
  3. During concurrent INSERT operations, both transactions attempt to insert a new row with the same business key. The @Version field provides zero protection against duplicate inserts; only a database-level UNIQUE constraint on the business key can prevent duplicate records.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“The @Version annotation in JPA activates Optimistic Locking to eliminate the Lost Update anomaly without physical database row locks. It should be placed on a numeric field (Long or Integer). On every UPDATE, Hibernate increments the version and validates WHERE id = ? AND version = ?. If another transaction modified the row first, 0 rows are updated and Hibernate throws OptimisticLockException. In Spring Data JPA, an object-typed @Version (Long) also solves the assigned ID problem: if version == null, Spring recognizes the entity as new and calls fast persist() instead of merge(). For bulk @Modifying queries, 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 @Version because it tells me when the record was updated.” — High-concurrency millisecond collisions make timestamps unreliable; always use Long or Integer.
  • ❌ “I write entity.setVersion(v + 1) in business service logic.” — Application code must never manually alter version numbers.
  • ❌ “Bulk @Modifying update queries automatically increment @Version.” — Bulk queries bypass the persistence context; you must increment the version explicitly in JPQL.
  • ❌ “An entity can have two @Version annotations.” — Prohibited by JPA; fails at bootstrap with AnnotationException.