Transaction Concerns

Read and Write Concerns Inside a Transaction

Concerns belong to the transaction, not to its operations. Pass readConcern and writeConcern to startTransaction or withTransaction; commands inside inherit them and cannot override them. readConcern: { level: "snapshot" } is the level you want: every read sees one majority-committed snapshot, so two reads of a document agree even if another client changes it between them. writeConcern: { w: "majority" } makes the commit durable on a majority of members before it returns.

A snapshot read, an outside reader, and a blocked outside writerJavaScript
const s = a.startSession();                      // ca and cb are two clients
s.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' } });
const inTxn = async () => (await ca.findOne({ _id: 'A' }, { session: s })).bal;
console.log('in txn  ->', await inTxn());
await ca.updateOne({ _id: 'A' }, { $inc: { bal: 5 } }, { session: s });
console.log('in txn  ->', await inTxn(), 'outside ->', (await cb.findOne({ _id: 'A' })).bal);
try { await cb.updateOne({ _id: 'A' }, { $inc: { bal: 1 } }, { maxTimeMS: 2000 }); }
catch (e) { console.log('outside write ->', e.codeName); }
await s.commitTransaction();
console.log('committed, bal =', (await cb.findOne({ _id: 'A' })).bal);
Output
in txn  -> 100
in txn  -> 105 outside -> 100
outside write -> MaxTimeMSExpired
committed, bal = 105

Three things are visible. The transaction reads its own uncommitted write, 105. The other client still reads 100, because uncommitted data is invisible everywhere else. And that client's write to the document does not fail outright — it waits for the lock the transaction holds, here until its own maxTimeMS ran out. Set a maxTimeMS on writes that may collide with a long transaction, or they block for its full lifetime.

A snapshot is consistent, not current, so reading, deciding and then writing can still be wrong. Encode the invariant in the filter — updateOne({ _id: 'A', bal: { $gte: 25 } }, ...) — and check modifiedCount, letting the write enforce the rule instead of your if.