Skip to main content

Transactions & Consistency

"MongoDB can't do transactions" is the most outdated thing people still say about it. It can — fully ACID, across documents and even shards. The real skill is knowing when you don't need them, because the document model often makes the question disappear.

The default you already have: single-document atomicity

Every write to a single document is atomic — all of it lands or none of it does, even when it touches nested fields and arrays at once.

db.accounts.updateOne(
{ _id: 1 },
{ $inc: { balance: -100 }, $push: { history: { type: "debit", amt: 100 } } }
)

That balance change and the history append can't half-happen. This is why the embed-vs-reference decision matters so much: model related data into one document and you get atomicity for free — no transaction needed.

When you genuinely need more: multi-document transactions

Some invariants span documents — the classic being a transfer that debits one account and credits another. Since 4.0 (replica sets) and 4.2 (sharded clusters), Mongo gives you real multi-document ACID transactions:

const session = db.getMongo().startSession()
session.startTransaction()
try {
const accts = session.getDatabase("bank").accounts
accts.updateOne({ _id: "A" }, { $inc: { balance: -100 } })
accts.updateOne({ _id: "B" }, { $inc: { balance: 100 } })
session.commitTransaction() // both, atomically
} catch (e) {
session.abortTransaction() // or neither
} finally {
session.endSession()
}

Under the hood it's the WiredTiger snapshot from the storage page: the transaction sees a frozen point-in-time view, and on commit all changes become visible together.

Transactions aren't free — treat them as the exception

They hold locks and a snapshot for their duration (default 60-second limit), which costs memory and can abort under write conflicts. The MongoDB-native instinct should be: first try to redesign so the data lives in one document. Reach for a transaction only when the invariant truly spans collections.

The consistency knobs

Transactions sit on top of three settings that control the durability/freshness trade-off. Two you met in Scaling; here they are together:

KnobQuestion it answersCommon choices
Write concern (w)how many nodes must ack before the write is "done"w:1 (fast), w:"majority" (durable through failover)
Read concernhow fresh/committed must the data I read belocal, majority (won't be rolled back), snapshot (txn point-in-time), linearizable
Read preferencewhich node serves the readprimary (fresh) vs secondaries (scales reads, may lag)

The pairing that matters: w:"majority" + read concern "majority" is the combo that guarantees you never read a write that a failover could later erase.

Sessions & causal consistency

All of the above rides on a logical session — the same session object that wraps a transaction. Sessions also unlock causal consistency: within one session, you're guaranteed read-your-own-writes and monotonic reads even if your write went to the primary and your next read goes to a lagging secondary. The session carries a logical timestamp so the secondary waits until it has caught up to what you already saw.

Retryable writes — the safety net you didn't configure

Modern drivers turn on retryable writes by default: if a single write fails due to a transient network blip or a primary election, the driver retries it exactly once, safely. A _id and operation token make the retry idempotent, so you don't get a double-insert. It's the reason a brief failover usually looks like a tiny pause rather than an error.

Recap

A single-document write is always atomic — model data together and you rarely need more. When an invariant spans documents, multi-doc ACID transactions (4.0+/4.2+) exist, but use them sparingly. Tune durability vs freshness with write/read concern (majority + majority is the safe pair), get read-your-writes across nodes via causal consistency in a session, and lean on default retryable writes to ride out elections.

👉 Next: Operating MongoDB