skip to content

JPA's @Version annotation can be placed on an integer-typed field or on a timestamp-typed field. What are the practical differences, and which would you choose for a table written by several application instances?

level: middleimportance: should knowfreq 35%

answer

  1. Numeric = pure counter, no clock, no collisions
  2. Timestamp = JVM clock by default
  3. Column precision truncation -> equal versions
  4. Clock skew / NTP jumps across instances
  5. Want last-modified? separate column

basics

~20 s

A numeric version is a pure counter: monotonic, database-agnostic, no clock involved, and collisions are impossible. A timestamp version depends on clock resolution and, when generated in the JVM, on clock agreement between instances; column precision can round two updates to the same value. Prefer numeric; use timestamp only when the column must double as a human-readable last-modified.

solid answer

~50 s

**Numeric** (`int`, `long`, `short` and their wrappers): Hibernate simply stores loaded + 1. It cannot collide, is portable across databases, is compact, and its meaning is unambiguous. Overflow is a non-issue with `long` and practically so with `int`. **Timestamp** (`java.sql.Timestamp`, `LocalDateTime`, `Instant`): Hibernate generates the value, by default from the **JVM clock**. Two risks follow. First, **precision** — if the column stores milliseconds or seconds while the JVM produces microseconds, two quick updates can round to the same stored value and a stale write can then match the predicate. Second, **clock skew or backwards jumps** across instances can produce a version that is not greater than the previous one. A timestamp version's one real advantage is that it doubles as a `last_modified` column that humans and reports can read. For a multi-instance system I use a numeric version, and add a separate audited `last_modified` column if the business needs one. Either way the field is written only by Hibernate; application code never assigns it.

code

java · 8 lines
java
@Entity
public class Account {
    @Id Long id;
    BigDecimal balance;

    @Version private long version;          // concurrency control
    private Instant lastModified;           // audit, maintained separately
}

go deeper

for a junior

Know that both types exist, that Hibernate writes the value, and that a plain numeric counter is the usual choice.

for a middle

Contrast collision-free counters with clock-dependent timestamps, naming precision truncation and skew as the concrete hazards, and mention the audit-column convenience.

for a senior

Add operational detail: database-sourced timestamps, migration defaults when introducing the column, null-version semantics for new entities, and exposing the version to clients as an opaque token.

for a principal

Argue for separating concerns — concurrency token versus audit timestamp — and set a standard so schema-wide behaviour is uniform and does not depend on per-node clock configuration.

## What both forms have in common Whichever type you choose, the mechanism is identical: the value is loaded with the entity, written into the `SET` clause with a new value and into the `WHERE` clause with the loaded value, and the affected-row count decides whether the write is accepted. JPA permits `int`, `Integer`, `short`, `Short`, `long`, `Long` and `java.sql.Timestamp`; Hibernate additionally supports `Instant`, `LocalDateTime`, `OffsetDateTime` and similar temporal types. ## Numeric versions Hibernate takes the loaded number and stores `loaded + 1`. Properties: - **No collisions.** Two different states of a row can never carry the same version unless something outside Hibernate wrote the column. - **No clock dependency.** Nothing about wall time, time zones, NTP steps or container clock drift can affect correctness. - **Portable and compact.** Every database has integers; no precision or type-mapping surprises. - **Overflow.** A `long` cannot realistically wrap. An `int` gives about two billion updates on a single row — enough in practice, and even if it wrapped, the failure mode requires two conflicting writers to be exactly 2^32 versions apart. - **Null means new.** A nullable wrapper version of `null` is how Hibernate can tell a detached instance is actually new — a detail that matters for `merge` and for `persist` versus `merge` decisions. Downside: a bare counter carries no information a human wants to read. ## Timestamp versions Hibernate generates the value at flush. By default the source is the **JVM clock** of the instance performing the write. Practical issues: - **Column precision.** If the entity produces microsecond precision but the column is `timestamp(3)` — or MySQL's default `DATETIME` with zero fractional digits — the stored value is truncated. Two updates within the same tick then store the *same* version, and a stale writer's `where version = ?` can match, letting a lost update through. This is a genuine, occasionally-seen production bug on hot rows. - **Clock skew.** With several instances writing, a node whose clock lags can write a version older than the one already stored. Nothing breaks the equality check immediately, but the column stops being monotonic, which defeats anyone reading it as "last modified" and confuses debugging. - **Backwards jumps.** NTP corrections and virtualised clocks can move time backwards; a counter never does. - **Type mapping.** Time zones, `Timestamp` versus `Instant`, and database-specific temporal types add avoidable variance. Hibernate can be told to take the value from the **database** rather than the JVM (historically `@Source(SourceType.DB)`, now expressed with `@CurrentTimestamp` on the version attribute), which removes cross-node skew at the cost of an extra round trip or a database function call — but not the precision problem. Its advantage is real but narrow: one column serves both concurrency control and "when was this last changed", which is convenient for support staff and simple audit needs. ## Choosing For a table written by multiple application instances I choose **numeric**, because correctness should not depend on clocks or column precision, and because the concurrency-control column and the audit column have different jobs. If the business wants a visible modification time, add `last_modified` maintained separately (application, trigger, or auditing framework) and keep the version a counter. I would consider a timestamp version only when: the schema already has one and is widely consumed, the write rate per row is low enough that precision collisions are implausible, and the value is sourced consistently (database-generated, or one writer). ## Rules that apply to both - Never set the version in application code, and never expose a setter that business logic might call — assigning it defeats the check silently. - Do not include the version in `equals`/`hashCode`; it changes over an object's lifetime. - Adding a version column to an existing table needs a default (`0` or a fixed timestamp) so existing rows are usable; a `NOT NULL` column with no default will break inserts and loads. - The version may be safely exposed to clients (form field, ETag) as an opaque token; a counter is easier to transport and compare than a timestamp with formatting and precision hazards. - Bulk JPQL and native updates do not maintain either form unless you write the column explicitly (or use HQL's `update versioned` form).

  • How exactly can a timestamp version let a lost update slip through?
    If the database column stores less precision than the generated value — for example seconds or milliseconds versus microseconds — two updates occurring within one tick are stored as the same value. A transaction that loaded the earlier state then finds `where version = ?` matching the row, its update affects one row, and the concurrent change is silently overwritten.
  • What must happen when you add a version column to an existing table?
    The column needs a sensible default so existing rows are valid — 0 for a counter, or a fixed past timestamp — otherwise loads and inserts fail on null. A forward-only migration that adds the column with a default and backfills is the standard approach; nullable numeric versions also carry the meaning "entity is new", so a null on an existing row can confuse merge behaviour.

saying these in an interview costs you the question

  • Assuming a timestamp version is safe because "time always moves forward" — NTP corrections and clock skew across nodes say otherwise.
  • Ignoring database column precision and its truncation of generated values.
  • Setting or updating the version field in application code, or exposing it to a mapper that copies it blindly.
  • Including the version in equals/hashCode.
  • Believing an int version realistically overflows in normal application workloads.

context