Two clients can read the same database row, derive different changes, and then write in sequence. If each update replaces values without checking the state that produced its decision, the later write can silently erase part or all of the earlier one. The database has serialized the statements, yet the application-level read-modify-write operation has still lost a concurrent change.
A version column changes the admission rule for the write. The update is accepted only if the row still carries the version observed by the client. A competing update advances that version, so a stale writer affects zero rows instead of overwriting newer state.
This pattern is commonly called optimistic concurrency control because it allows work to proceed without holding a database lock across the entire application operation. Its central property is narrower: a write can prove that its input state has not been superseded at the chosen row boundary.
The predicate connects a read to a later write
Consider an account profile stored with an integer version:
id: 42
email: old@example.test
display_name: Mira
version: 7Two requests read version 7. One changes the email address and commits first. Its mutation can use a conditional statement:
UPDATE profiles
SET email = 'new@example.test',
version = version + 1
WHERE id = 42
AND version = 7;If the statement succeeds, the row advances to version 8. A second request that still carries version 7 can issue its own conditional update, but its predicate no longer matches. The database reports zero affected rows.
The failed predicate is the useful event. It tells the application that the state used to prepare the mutation is stale. Without that predicate, the second request may commit normally while replacing data derived from an obsolete snapshot.
The version value is therefore not merely metadata for display or auditing. It participates in the write condition and must advance atomically with the protected mutation.
Statement atomicity closes the check-write gap
A separate read of the current version immediately before an unconditional update does not provide the same guarantee.
Code shaped like this has a race:
read current version
if current version equals expected:
issue unconditional updateAnother transaction can modify the row after the check but before the update. The application has split validation and mutation into two independently interleavable operations.
Putting the expected version in the UPDATE predicate lets the database evaluate eligibility as part of the mutation statement. Normal database concurrency control determines which row version is eligible when the statement executes. The application receives the outcome through the affected-row count or an equivalent API result.
The same principle can be expressed with other conditional mutation facilities. The important property is atomic comparison and state change at the storage boundary, not the particular name or type of the version field.
A version counter represents change, not elapsed time
An integer counter has simple comparison semantics: each accepted mutation moves the row to another generation. The application usually needs equality against the generation it observed, rather than a claim about real-world time.
Timestamps can also serve as concurrency markers in some designs, but their correctness depends on how values are generated and on whether distinct accepted mutations can receive indistinguishable values. Clock precision, clock source, database defaults, and application-generated timestamps can all affect that property.
A counter avoids clock semantics. It does not need to represent when a change happened. It only needs to change in a way that prevents an old observation from matching after a protected mutation.
Opaque revision identifiers can provide the same stale-write detection if every relevant mutation replaces the identifier and clients compare for exact equality. Ordering is not required when the operation asks only whether the revision is still the one previously observed.
Conflict detection does not choose a merge policy
Rejecting a stale update preserves evidence of concurrency, but it does not decide the final application state.
After a conflict, an application can reload the current row and abandon the attempted change. It can ask a caller to resubmit against fresh state. It can recompute a deterministic mutation. In some domains it can merge non-overlapping edits, provided the merge rules are explicit and valid for the data model.
Blindly retrying the same replacement values can defeat the protection. Suppose a request read version 7, calculated a complete replacement document, then encountered version 8. Reloading only the version number and resending the old replacement against version 8 can erase the concurrent change just as an unconditional update would. The retry has satisfied the mechanical predicate while retaining stale business input.
A safe retry must reconsider the mutation against current state when the operation’s meaning depends on that state.
This distinction separates conflict detection from conflict resolution. The version column provides a reliable signal that a decision was based on superseded data. Application semantics determine what to do with that signal.
Row granularity defines the protected invariant
A version on one row detects changes to that row. It does not automatically protect an invariant spanning several rows.
Suppose a scheduling rule says that at most one active reservation may exist for a resource. Two transactions can read different rows or an empty result set, then insert separate rows. A version field on each inserted row cannot detect the conflict because neither transaction is updating a shared versioned record.
The invariant needs a concurrency mechanism at a boundary both operations contend on. Depending on the data model and database, that can be a unique constraint, a locked parent row, a serializable transaction, a dedicated coordination record, or another mechanism whose semantics cover the invariant.
The same issue appears when an operation reads row A but writes row B. A version predicate on B says that B has not changed since its expected version. It says nothing about whether A, which influenced the decision, has changed.
Version checks are strongest when the state read to make the decision and the state conditionally mutated share the same concurrency boundary.
Partial updates and version checks solve different problems
Updating only changed columns can reduce accidental overwrites, but it is not equivalent to detecting stale input.
If one request changes email and another changes display_name, two narrow SQL updates may both produce an acceptable combined result. In that specific data model, field-level independence can make the operations commute.
But a narrow update can still be invalid when its decision depended on another field. A request might change status only after reading balance, even though its SQL statement writes no balance column. If balance changes concurrently, column-level write separation does not reveal that the status decision used stale state.
A version predicate makes the dependency conservative by treating any protected row mutation as a reason to reject an operation based on the prior generation. That may create conflicts between edits that could have coexisted, but it also avoids pretending that written-column overlap fully describes application dependencies.
More granular concurrency markers are possible, yet they require a correspondingly precise model of which state each operation depends on.
Every mutation path has to advance the generation
The mechanism fails if some writers modify protected state without changing the version.
An application service might consistently increment version, while an administrative script performs direct updates that leave it untouched. A client holding the old version can then pass its predicate even though the row changed after its read.
Database triggers can centralize revision advancement in systems where that fits the write model. Application-managed counters can also be correct when every relevant mutation path follows the contract. The implementation choice matters less than completeness: any change that should invalidate stale decisions must alter the concurrency marker.
Bulk updates, maintenance jobs, data imports, stored procedures, and alternate services deserve the same scrutiny as ordinary request handlers. A concurrency token protects only the mutations included in its generation discipline.
Version checks make stale authority observable
A lost update is difficult to handle after the overwrite has already committed because the later write can erase the evidence that another decision intervened. Conditional updates move the decision earlier. The storage engine either accepts a mutation against the expected generation or exposes a conflict before the stale write takes effect.
That guarantee remains deliberately local. A version column does not serialize arbitrary workflows, merge concurrent intent, protect multi-row invariants by itself, or make retries automatically safe. It gives one precise property: an operation tied to an older protected generation cannot silently replace a newer one through the same conditional mutation path.
That property is often enough to turn an ambiguous overwrite into an explicit branch in application behavior. The conflict becomes data the application can handle rather than history the database has already discarded.