← All articles

Preventing silent overwrites with a revision check

Concurrent editors should not silently overwrite one another. Include the version originally read by the client in the update condition.

t.Turgay
Preventing silent overwrites with a revision check

Two tabs open the same record. Tab A saves an edit, then tab B submits a different value from its older view. Silently accepting the last write may look simple while losing A's work. The revision approach used by Portfolio asks which version the caller edited and makes that version part of the write condition.

Make the expected version part of UPDATE

The PostgreSQL demonstration uses a temporary table and does not modify application tables. Its jsonb payload keeps the example compact; Portfolio's actual persisted content is relational. The concurrency idea applies to either representation.

CREATE TEMP TABLE revision_demo(
 id integer PRIMARY KEY, revision integer NOT NULL, payload jsonb NOT NULL);
INSERT INTO revision_demo VALUES(1,1,'{"title":"first"}');
UPDATE revision_demo SET payload='{"title":"tab-a"}', revision=revision+1
 WHERE id=1 AND revision=1 RETURNING revision;
UPDATE revision_demo SET payload='{"title":"tab-b"}', revision=revision+1
 WHERE id=1 AND revision=1 RETURNING revision;
SELECT payload->>'title',revision FROM revision_demo;

The first UPDATE matches revision one, writes tab-a and returns revision two. The second still expects one and affects no rows. The final result is tab-a with revision two. Zero affected rows is not a SQL syntax error; it is a conflict outcome the application must interpret correctly.

A prior SELECT does not close the race

Reading a revision and then updating by ID alone leaves a window for another writer. Including the revision predicate in UPDATE makes the acceptance decision atomic. PostgreSQL locking and isolation behavior must be understood alongside that predicate; an application-side precheck is not a substitute.

RETURNING supplies the accepted new revision for the next edit. If the server returns an apparent success after a failed condition, the UI can show a save that never persisted. The success result must represent the committed operation rather than the attempted request.

Choose the version's scope deliberately

Revisions can belong to individual records or a complete editing document. Portfolio's panel submits profile, project and article edits through one save contract, so it protects that aggregate document. Independent edits in different sections can therefore conflict. The corresponding benefit is acceptance or rejection of the complete submitted edit set.

When the write spans several tables, revision advancement and related writes need one transaction. A new revision must not remain after a child-table failure. Persistence, commit and the returned result have to describe the same outcome.

Preserve the draft after a conflict

Automatically replacing the expected revision with the newest value and resending can defeat the protection. Keep the draft, read the latest version separately if needed and let the user resolve the difference. A network failure should not clear the editor either.

Typing during an in-flight request introduces another version boundary. If request A is saving while the user enters text B, A's response must not replace B. Retain a saved snapshot separately from the current draft and mark the editor clean only when the completed request still represents the same edit version.

A revision is not an authorization credential

Knowing the version grants no permission to edit. Authentication and resource scope remain server checks. The distinction between deleted and stale records should also follow the application's disclosure policy. ETag/If-Match can express a similar HTTP contract, but whichever model is chosen must be consistent between client and server.

Test two edits starting from the same revision: only one should persist, and the other should receive an explicit conflict. Follow the success with a fresh read and verify publication flags and source dates. Automatic retries belong only at boundaries whose operations can safely be re-executed.

PostgreSQL UPDATE and RETURNING; PostgreSQL transaction isolation.

✳ You have reached the end of this orbit.
Explore more articles →
turgay.A little code, a few pixels, a lot of curiosity.© 2026 · Made on Earth ✳