Concurrency control
Reject an edit based on an old revision
Carry the version a user actually edited into the save request. Compare it atomically with the current record before replacing data.
In this article
Return a revision with the editable record
When loading an editable project, return its current revision alongside the fields. Keep that revision associated with the user's draft.
If the application refreshes data in the background, do not silently replace the draft's revision while leaving its older field values unchanged. That would make an old edit appear current to the server.
Decide which changes advance the revision. Every writer that can affect the protected representation must participate, including import jobs and support tools.
Compare and update in one operation
Send the observed revision with the mutation and include it in the database write condition. A separate read followed by an unconditional update leaves another race between those statements.
UPDATE projects
SET name = :name,
revision = revision + 1
WHERE tenant_id = :tenant_id
AND id = :project_id
AND revision = :expected_revision
RETURNING revision;This illustrative statement assumes access checks and input validation occur in the appropriate request boundary. The tenant condition scopes the record but does not prove the caller is authorised to edit it.
Check whether a row was returned. No row means the expected update did not occur. Resolve missing, inaccessible and conflicting records according to the API's disclosure policy rather than leaking existence through detailed errors.
Preserve the user's work on conflict
Return a defined conflict response and keep the draft available. Offer the current version for comparison when the caller is authorised to read it.
Do not automatically fetch the latest revision and resend the same replacement values. That bypasses the protection and overwrites changes the user has never reviewed.
For independent fields, a deliberate merge may be possible. For a long text field or a business decision, ask the user to resolve the difference. The merge policy should follow the data's meaning.
Test more than two browser tabs
Exercise the normal form against a background import that updates the same record. Confirm that both paths advance and compare revisions as intended.
Test a timeout after the update commits. A subsequent retry may need an operation identity to recognise the already completed mutation rather than reporting a misleading edit conflict.
At the HTTP boundary, conditional requests can express version preconditions using validators. Whichever interface is chosen, keep the database enforcement atomic. A correctly named header does not itself prevent an unconditional write in the application.
Primary sources
HTTP semantics: conditional requestsReferences checked 11 September 2026.