Two editors can read the same document and submit different changes later. Each individual update can be atomic while the second editor still overwrites work based on an obsolete snapshot. Optimistic concurrency addresses that lost-edit problem by including an expected version in the update filter. The version check and version increment occur in one document update. This MongoDB 8.0 reference extends basic atomic increments into an application-wide editing protocol. Its complete mongosh fixtures simulate two actors sequentially with stale snapshots, which reproduces the logical conflict deterministically without relying on a particular thread race.
Identify the lost-update problem
Atomicity means a single-document write is not observed half-applied. It does not mean every earlier read remains current until a later write. If two users read version one of a draft, user A can save first and user B can then submit a full replacement based on the old draft. Without a condition, B's write can erase A's change even though both writes individually satisfy atomicity. A last-write-wins policy may be intentional for some data, but it should not be mistaken for conflict detection.
A version token records which state an editor used. Read the fields and version together, return the version to the editing client, and require it when saving. Treat the token as a concurrency condition, not an authorization credential. The server must still identify the document's owner and validate allowed fields. A client should not be permitted to choose arbitrary version changes or update operators merely because it supplied a version value.
Compare and advance in one update
Place both the document identity and expected version in the update filter. Set the approved changed fields and increment the version in the same update. If the current version no longer matches, the filter selects no document. This is a compare-and-set pattern: the database evaluates whether the state still matches and applies the new state atomically for that document. Checking the version in an earlier find and then writing only by _id reintroduces the gap between check and update.
Every writer participating in this editing contract must advance the version. A maintenance script that changes fields without updating version can make stale edits appear current. Schema design and write interfaces should enforce the protocol consistently. Choose a suitable numeric representation and migration policy for legacy documents without versions. Avoid upsert on a stale-editor filter unless a carefully defined create behavior requires it; otherwise a failed match can attempt an unintended insertion and turn a conflict into a confusing duplicate-key error.
Interpret matchedCount as the concurrency result
For updateOne, matchedCount tells whether the expected state was selected. A zero count can indicate a stale version, a nonexistent document, or a document outside the authorization scope included in the filter. Do not reveal existence to an unauthorized caller merely to provide a detailed conflict message. In an authorized editing interface, return a documented conflict result and offer a fresh representation or a reload route. HTTP APIs commonly express such a conflict with a suitable status, but the exact product response belongs to the API contract.
modifiedCount is a different measure. It reports actual modifications, and applications should not generally equate zero modifications with a failed filter match. This protocol increments version on an accepted edit, so a matching valid update normally changes the document even if a submitted field is unchanged. The main fixture prints matchedCount for each simulated editor, then the stored title and version. That directly demonstrates accepted versus stale work instead of inferring success from the absence of an exception.
Resolve conflicts rather than hiding them
Automatically rereading the latest version and resubmitting an obsolete full object can simply recreate last-write-wins under a new name. Decide whether to ask the user to compare changes, reject the edit, or merge a clearly independent patch. A merge needs field ownership and semantic rules. Two changes to different fields may be compatible, while two changes to the same field or to related business constraints may require human or domain-specific resolution.
The practice intentionally rebases a display-name patch after a separate city change. It rereads the latest version, applies only the approved display-name field, and preserves the city. This is a demonstrated nonconflicting case, not a universal merge algorithm. Multi-document business invariants may still require transactions or another coordination design; a version on one document does not serialize an entire system. Also record enough information for a client to show a meaningful conflict rather than silently dropping the user's attempted change.
MongoDB documentation and version notes
MongoDB Optimistic Concurrency Control Examples
Each example shows the code, the output it produces and what each part does.
Rejecting a stale editor's versioned update
Run the complete script in a disposable MongoDB 8.0 fixture database. Both simulated editors begin with the same stored version. Editor A updates the title and increments the version. Editor B then uses its stale version and matches no document. The final projection shows A's title and version two, proving that the stale update did not overwrite the accepted edit. The sequential test represents the same read-before-write relationship as overlapping users, while remaining reproducible. No transaction is needed because the comparison and mutation concern one document and occur in one update operation.
const drafts = db.getSiblingDB('codevra_ref_versions').getCollection('drafts');
drafts.deleteMany({});
drafts.insertOne({ _id: 1, title: 'Draft', version: 1 });
const editorA = drafts.findOne({ _id: 1 });
const editorB = drafts.findOne({ _id: 1 });
const first = drafts.updateOne({ _id: 1, version: editorA.version },
{ $set: { title: 'Accepted A' }, $inc: { version: 1 } });
const second = drafts.updateOne({ _id: 1, version: editorB.version },
{ $set: { title: 'Stale B' }, $inc: { version: 1 } });
print(first.matchedCount);
print(second.matchedCount);
const stored = drafts.findOne({ _id: 1 });
print(JSON.stringify({ title: stored.title, version: stored.version }));1
0
{"title":"Accepted A","version":2}MongoDB Optimistic Concurrency Control in Real Projects
Rebase an independent patch after a version conflict
Simulate a city edit and a stale display-name edit on one profile. Detect the stale attempt, then deliberately rebase only the independent display-name patch against the latest version. The solution preserves the accepted city change and advances version again. This task demonstrates an explicit merge decision: the fields are independent under the fixture's stated contract. If both edits changed city, or if those fields participated in a shared validation rule, automatic rebasing would need a different policy. Keep the fresh version check in the final update even after rereading, since another writer could change the document between that read and the attempted merge.
const profiles = db.getSiblingDB('codevra_ref_versions').getCollection('profiles');
profiles.deleteMany({});
profiles.insertOne({ _id: 1, displayName: 'Ada', city: 'Leeds', version: 1 });
const stale = profiles.findOne({ _id: 1 });
profiles.updateOne({ _id: 1, version: 1 }, { $set: { city: 'York' }, $inc: { version: 1 } });
const rejected = profiles.updateOne({ _id: 1, version: stale.version },
{ $set: { displayName: 'Grace' }, $inc: { version: 1 } });
print(rejected.matchedCount);
const latest = profiles.findOne({ _id: 1 });
const merged = profiles.updateOne({ _id: 1, version: latest.version },
{ $set: { displayName: 'Grace' }, $inc: { version: 1 } });
print(merged.matchedCount);
const row = profiles.findOne({ _id: 1 });
print(JSON.stringify({ displayName: row.displayName, city: row.city, version: row.version }));0
1
{"displayName":"Grace","city":"York","version":3}MongoDB Optimistic Concurrency Control FAQ
Why is atomic update not enough to prevent stale edits?
Atomicity protects the write itself, not the freshness of a separate earlier read. A later write can atomically overwrite data based on an old snapshot. Include the expected version in the same update filter that performs the mutation. That lets the server reject stale state at the operation boundary. A separate preflight version check followed by an unconditional write does not close the race.
Should I check matchedCount or modifiedCount?
Check matchedCount to determine whether the expected state was selected. modifiedCount describes whether values changed and has a different meaning. This protocol increments version on acceptance, making the state transition observable. Also distinguish a stale edit from an absent or unauthorized document according to your API's disclosure policy. Do not use the version token as proof that the caller may edit the record.
Can I fix a conflict by replacing with a fresh version number?
Only after a deliberate resolution policy. Blindly resubmitting the old full object under the latest version can still erase another user's work. The practice merges one independent field patch while preserving the changed city, and it performs another conditional update. Conflicting fields or coupled business rules need comparison or domain-specific handling. The version mechanism detects conflicts; it does not choose the correct merged content.
Must background scripts increment version too?
Yes, if their changes participate in this editing protocol. A writer that changes protected fields without advancing version can make an obsolete client snapshot appear current. Centralize or document approved write paths and migrate older data consistently. The requirement applies to maintenance jobs as well as UI saves. A version field is useful only when all relevant writers honor its meaning.
MongoDB Optimistic Concurrency Control Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
After the worked independent-field merge, what city remains?
MongoDB Optimistic Concurrency Control Quiz
2 questions to check what you learned.
Where must the expected version check occur?
- In the filter of the same update that changes fields and advances version
- Only in a preceding find before an unconditional update
- Only in the client UI with no server condition
Why avoid upsert for an ordinary stale-editor save?
- A failed version match should report a conflict, not attempt to create another record
- Upsert disables every unique index
- Upsert automatically merges all stale fields correctly