Optimistic Locking
A row that two callers update concurrently can lose one write — both readbalance = 100, both compute balance + 10, both write balance = 110,
and the bank is short ten dollars. Optimistic locking detects this at the
database boundary and rejects the second write with 412 Precondition Failed, leaving the row untouched.
Schema attribute
Add@version to one Int field per model:
- exactly one
@versionfield per model - type must be required
Int - cannot also be the primary key
i64 at runtime.
Update flow
Every successful update emitsversion = version + 1 in the same SQL
statement that writes the new state. The generated REST router:
- returns
ETag: "<version>"onGET /resource/<id>and on the response of any successful mutation - requires
If-Match: "<version>"onPATCH /resource/<id>and onDELETEfor soft-delete models - responds
412 Precondition FailedwhenIf-Matchis missing - responds
412 Precondition Failedwhen the supplied version is stale - distinguishes “stale version” from “row not found” by probing the read policy after the update fails
Programmatic use
Internal Rust callers thread the expected version throughif_match:
if_match on a versioned model returns CoolError::PreconditionFailed
before any SQL runs. Banks treat the version check as a contract, not a
hint — there is no “force update” escape hatch on the generated path.
Input filtering
@version is excluded from both Create<Model>Input and Update<Model>Input:
- clients cannot seed the initial version on create — the framework sets it to
0 - clients cannot replay or skip a version through a PATCH body — the column is bumped server-side
Interaction with @@soft_delete
Soft-delete tombstones bump the version column too, so callers that re-read
after a delete observe a fresh ETag. Live updates against a tombstoned
row match zero rows and return the same “precondition failed” shape.
When to use it
Add@version to:
- balances, ledger entries, transfers, holds, reservations
- any row a workflow reads, decides on, then writes back
- any row a webhook can update concurrently with a user-facing flow
- append-only event tables
- rows updated by exactly one writer (lookup tables, configuration)
- denormalised counters that already use SQL-level atomic increments
Read Next
- Idempotency — protects against duplicate execution; complements lost-update protection
- Transaction isolation — closes the read-write skew window inside the same transaction
- Field attributes — full list of supported field attributes