Optimistic concurrency detects that the facts behind a decision have changed. A conflict should trigger a new decision, not a blind retry.
Optimistic Concurrency Guard
A decision is valid only against the version of state it observed.
UPDATE subscriptions
SET plan = $1, version = version + 1, updated_at = now()
WHERE id = $2 AND version = $3
RETURNING *;
-- Zero rows means conflict: reload state; do not silently retry the decision.Invariant: A write succeeds only against the exact version the decision observed.
Use when: Two writers may update the same aggregate and lost updates are unacceptable.
Why this boundary matters
A version predicate turns a silent lost update into an explicit conflict. The business decision must then be recomputed against current state.
Failure policy
| Boundary | Action |
|---|---|
| Version matches | Commit the update and increment the version |
| Version mismatch | Return a conflict and reload current state |
| Automatic retry requested | Re-run the business decision, not only the SQL |
| Aggregate is consistently hot | Reconsider partitioning, serialization, or pessimistic locking |
| External side effect follows update | Use an outbox or equivalent atomic handoff |
Trade-offs
Optimistic concurrency avoids blocking when conflicts are rare, but repeated conflicts waste work and hurt latency on hot aggregates. It detects stale decisions; it does not merge them.
Decision rule: Use optimistic control when conflicts are exceptional and the caller can explicitly resolve or recompute a stale decision.