Trace the complete transaction lifecycle through a logical session, including visibility, abort, commit, callback retries, and commit ambiguity.

Logical Sessions, Transaction Start/Commit/Abort, and Driver Transaction APIs

Follow one logical session through start, statement visibility, commit, abort, and driver-managed retry behavior without duplicating external side effects.

Intermediate110–160 minutesSession + commit/abort + PyMongo callback labMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning objectives

01

Define logical sessions and explain how transactions are associated with a session and transaction number.

02

Trace startTransaction(), statement execution, commitTransaction(), and abortTransaction().

03

Observe transaction invisibility outside the session before commit and committed state afterward.

04

Distinguish driver callback APIs from lower-level explicit transaction APIs.

05

Explain why callback retries make non-database side effects dangerous unless they are idempotent or deduplicated.

Reproducible lab baseline

This lesson pins MongoDB Community Server 8.3.8 with mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim, mongosh 2.10.0, and PyMongo 4.17.0 where a driver example is used. Transactions require a replica set or sharded cluster, so the mandatory local lab runs a disposable replica set published only on loopback beginning at 127.0.0.1:27078. Authentication and TLS are disabled only for this isolated learning topology. Feature Compatibility Version (FCV) is observed and never changed. Unless a section explicitly overrides them, read/write concern and read preference use the transaction/client defaults described beside the example. Atlas, Search, Vector Search, KMS, and Enterprise Advanced are not mandatory. The one-member replica set can prove transaction atomicity and visibility, but it cannot prove multi-node majority durability or failover behavior. The product commands were not executed in this generation environment because Docker, mongod, mongosh, and PyMongo are unavailable here; measured output must be recorded on the learner's machine.

1. AtlasMart problem: transfer value between independently owned wallets

A logical session groups related client operations and provides the server-side framework for causal consistency, retryable writes, and transactions. A multi-document transaction is identified by session information such as lsid plus a transaction number (txnNumber) in server observability. ACID abbreviates atomicity, consistency, isolation, and durability; MongoDB transactions provide the database transaction boundary, while application correctness still depends on valid business predicates and retry design.

Wallet A and Wallet B have independent lifecycle ownership. Embedding every customer's wallet into one shared document would create an unreasonable hot aggregate, so a real cross-document transaction is justified for an atomic transfer.

bash · isolated one-member replica-set setup for Lesson 2
docker rm -f atlasmart-mongo-ch13-l2 2>/dev/null || truedocker volume rm atlasmart-mongo-ch13-l2-data 2>/dev/null || truedocker run -d --name atlasmart-mongo-ch13-l2 \  -p 127.0.0.1:27078:27017 \  -v atlasmart-mongo-ch13-l2-data:/data/db \  mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim --replSet atlasmart-rs13-l2 --bind_ip_alluntil mongosh "mongodb://127.0.0.1:27078/admin?directConnection=true" --quiet --eval \'quit(db.runCommand({ping:1}).ok === 1 ? 0 : 1)'; do sleep 1; donemongosh "mongodb://127.0.0.1:27078/admin?directConnection=true" --quiet --eval \'rs.initiate({_id:"atlasmart-rs13-l2",members:[{_id:0,host:"localhost:27017"}]})'until mongosh "mongodb://127.0.0.1:27078/admin?directConnection=true&replicaSet=atlasmart-rs13-l2" --quiet --eval \'quit(db.hello().isWritablePrimary ? 0 : 1)'; do sleep 1; donemongosh "mongodb://127.0.0.1:27078/admin?directConnection=true&replicaSet=atlasmart-rs13-l2" --quiet --eval \'printjson({server:db.version(),setName:db.hello().setName,primary:db.hello().isWritablePrimary}); printjson(db.runCommand({getParameter:1,featureCompatibilityVersion:1,transactionLifetimeLimitSeconds:1,maxTransactionLockRequestTimeoutMillis:1}))' 

2. Create existing collections before transaction work

The fixture is created before the transaction. This keeps the lesson focused on transaction mechanics rather than Data Definition Language (DDL) rules. DDL means operations that create or change collections, indexes, or database metadata; Lesson 4 treats those restrictions separately.

javascript · seed two independent wallet documents
const w=db.wallets_ch13_l2;w.drop();w.insertMany([  {_id:"wallet-a",tenantId:"tenant-a",balanceCents:10000},  {_id:"wallet-b",tenantId:"tenant-a",balanceCents:2500}]);printjson(w.find().sort({_id:1}).toArray());

3. Start, observe, and commit

Session.startTransaction() configures the transaction locally; the server-side transaction actually starts on the first command sent with that session. Operations must use the session-bound database/collection. Until commit, writes are not visible outside the transaction. The $currentOp pipeline can expose an inactive transaction with its lsid, txnNumber, read concern, timestamps, and elapsed time when that session is reportable.

javascript · explicit mongosh transaction lifecycle
const session=db.getMongo().startSession({causalConsistency:true});const sdb=session.getDatabase("atlasmart");const wallets=sdb.wallets_ch13_l2;session.startTransaction({readConcern:{level:"snapshot"},writeConcern:{w:"majority"}});const debit=wallets.updateOne({_id:"wallet-a",balanceCents:{$gte:2000}},{$inc:{balanceCents:-2000}});if (debit.modifiedCount !== 1) throw new Error("insufficient funds");wallets.updateOne({_id:"wallet-b"},{$inc:{balanceCents:2000}});print("outside session before commit:");printjson(db.wallets_ch13_l2.find().sort({_id:1}).toArray());print("transaction observability where the idle session is exposed:");printjson(db.getSiblingDB("admin").aggregate([  {$currentOp:{allUsers:true,idleSessions:true}},  {$match:{transaction:{$exists:true}}},  {$project:{_id:0,type:1,lsid:1,transaction:1}}]).toArray());session.commitTransaction();print("outside session after commit:");printjson(db.wallets_ch13_l2.find().sort({_id:1}).toArray());session.endSession();
Expected state

Before commit, the ordinary db.wallets_ch13_l2 read should still show 10000 and 2500. After commit, it should show 8000 and 4500. The $currentOp shape is version/runtime dependent; an empty result is not evidence that no transaction exists, because only reportable active/idle transaction state appears.

4. Abort on a business precondition failure

An application error is also a reason to abort. The impossible debit is detected inside the transaction; the catch block aborts and the outside collection remains unchanged. The transaction does not commit a prefix of its writes.

javascript · controlled business abort
const session=db.getMongo().startSession();const sdb=session.getDatabase("atlasmart");const wallets=sdb.wallets_ch13_l2;session.startTransaction({readConcern:{level:"snapshot"},writeConcern:{w:"majority"}});try {  const r=wallets.updateOne({_id:"wallet-a",balanceCents:{$gte:999999}},{$inc:{balanceCents:-999999}});  if (r.modifiedCount !== 1) throw new Error("insufficient funds: abort business transaction");  wallets.updateOne({_id:"wallet-b"},{$inc:{balanceCents:999999}});  session.commitTransaction();} catch (e) {  try { session.abortTransaction(); } catch (_) {}  print("aborted:",e.message);}printjson(db.wallets_ch13_l2.find().sort({_id:1}).toArray());session.endSession();

5. Driver-managed callback API and retry danger

PyMongo with_transaction() manages start/commit/abort and incorporates transaction retry behavior. The callback may be invoked more than once if the driver retries the whole transaction. Therefore sending email, charging a non-transactional external payment API, incrementing an external counter, or publishing a message directly inside the callback can duplicate side effects. Keep the callback database-focused, or protect external effects with an idempotency/deduplication protocol.

python · PyMongo with_transaction callback
from pymongo import MongoClientfrom pymongo.errors import ConnectionFailure, OperationFailurefrom pymongo.read_concern import ReadConcernfrom pymongo.write_concern import WriteConcernfrom pymongo.read_preferences import ReadPreferenceuri = "mongodb://127.0.0.1:27078/?directConnection=true&replicaSet=atlasmart-rs13-l2"client = MongoClient(uri)wallets = client.atlasmart.wallets_ch13_l2def body(session):    debit = wallets.update_one(        {"_id": "wallet-a", "balanceCents": {"$gte": 100}},        {"$inc": {"balanceCents": -100}},        session=session,    )    if debit.modified_count != 1:        raise RuntimeError("insufficient funds")    wallets.update_one(        {"_id": "wallet-b"},        {"$inc": {"balanceCents": 100}},        session=session,    )    return "db-work-complete"with client.start_session() as session:    result = session.with_transaction(        body,        read_concern=ReadConcern("snapshot"),        write_concern=WriteConcern("majority"),        read_preference=ReadPreference.PRIMARY,    )    print(result)client.close()
Commit ambiguity

If the server commits but the network fails before the client receives the answer, the client cannot infer “not committed” from the missing response. Drivers handle an UnknownTransactionCommitResult by retrying the commit operation. A TransientTransactionError can require retrying the whole transaction. Retrying the entire business action from scratch is not the same thing.

Production judgment. Reuse the application's long-lived MongoClient; create bounded client sessions from it. A session must not be used concurrently. Every operation in an explicit transaction must be associated with that session. Monitor abort/retry rates and latency rather than hiding retries. Keep transaction bodies short, deterministic, and free from irreversible external side effects.

Bridge. Lesson 3 separates snapshot/read visibility from durability acknowledgement and proves snapshot behavior with two client connections.

bash · cleanup / full reset
docker rm -f atlasmart-mongo-ch13-l2docker volume rm atlasmart-mongo-ch13-l2-data

Check your understanding

  1. What identifies a server transaction in observability?
  2. When does startTransaction() actually start work on the server?
  3. Are uncommitted writes visible to ordinary outside reads?
  4. Why can with_transaction() duplicate an email sent inside its callback?
  5. What should be retried for UnknownTransactionCommitResult?
Review the answers

1. The logical session identifier (lsid) together with the transaction number identifies a transaction.

2. The method configures the transaction; the server transaction begins when the first command is sent on the session.

3. No. Transaction changes remain invisible outside the transaction until commit.

4. The driver may invoke the callback more than once when retrying the transaction.

5. The commit decision should be retried with the same transaction context; it does not mean the transaction definitely failed.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.