Transactions, write concerns and what you give up
Module 6 was about transactions and concurrency in PostgreSQL — ACID, isolation levels, the lost update. This lesson is the same territory in MongoDB, and the honest summary is: MongoDB can do much of what PostgreSQL does, but the defaults are different, some of it is opt-in, and some of it costs you a replica set. Knowing exactly what you get, and what you give up, is the difference between a correct system and a subtly broken one.
Single-document operations are atomic — and this is the foundation
The one guarantee you always have, on any deployment:
db.books.updateOne(
{ _id: 1 },
{ $inc: { copies: -1 }, $push: { history: "borrowed" } }
)
A write to a single document is atomic — all of it happens or none of it does, even when it
touches several fields and an array. Another reader never sees the copies decremented but the
history not yet pushed.
This is why the modelling lesson pushed you to embed. If the order and its line items are one document, adding a line item and updating the order total is one atomic write — no transaction needed. Good document design turns many would-be multi-document transactions into single-document ones. The embed-or-reference decision is partly a transactions decision, and now you can see why.
$inc deserves a second mention: it increments atomically on the server without you reading
the value first, so it is immune to the lost-update race from module 6. db.counters.updateOne({_id: "loans"}, { $inc: { seq: 1 } }) is a safe counter with no read-modify-write window.
Multi-document transactions exist — with a condition
Since version 4.0 MongoDB has real ACID transactions across documents and collections:
const session = db.getMongo().startSession();
session.startTransaction();
try {
const s = session.getDatabase("library");
s.books.updateOne({ _id: 1 }, { $inc: { copies: -1 } });
s.loans.insertOne({ book_id: 1, member_id: 3, borrowed: new Date() });
session.commitTransaction();
} catch (e) {
session.abortTransaction();
throw e;
} finally {
session.endSession();
}
That is BEGIN / COMMIT / ROLLBACK with extra ceremony. But there is a catch you will hit
immediately in development. Run it against a standalone mongod and:
This MongoDB deployment does not support retryable writes.
Multi-document transactions require a replica set (or a sharded cluster). A single standalone
server — the default docker run mongo — cannot do them. This surprises everyone once: your
transaction code is correct and fails anyway, purely because of the deployment topology. In
production you run a replica set regardless (for durability and failover), so it works there; in
local development you must start MongoDB as a replica set to test transaction code at all.
And even where they work, MongoDB's own guidance is pointed: transactions carry a performance cost, and needing them often is a sign the data should have been modelled to avoid them — that is, embedded. In PostgreSQL a multi-row transaction is routine and cheap; in MongoDB it is a tool you reach for deliberately, not the default posture. If you find yourself wrapping most writes in transactions, the data was relational and you are fighting the model.
Write concern — how many nodes must confirm
This is the knob PostgreSQL does not really expose, and it is a genuine correctness-versus-speed dial. A write can be acknowledged after reaching different numbers of nodes:
db.loans.insertOne(doc, { writeConcern: { w: "majority", j: true } })
w: 1— acknowledged when the primary has it. Fast. But if the primary crashes before the write replicates, a failover can lose that write. This is the default, and it is worth knowing that the default trades durability for speed.w: "majority"— acknowledged only when a majority of nodes have it, so it survives a failover. This is what you want for anything that matters — a payment, an order.j: true— the write is on disk (in the journal), not just in memory, before acknowledgement.
The default w: 1 means a MongoDB write can be lost on failover in a way a committed PostgreSQL
transaction cannot. That is not a bug; it is a default chosen for throughput. For important
writes, set w: "majority" and accept the higher latency. Naming the trade is the point:
PostgreSQL gives you durability by default and you rarely think about it; MongoDB makes you choose.
Read concern — what you are allowed to see
The mirror image, controlling read consistency:
local— whatever the node has, which on a replica might be rolled back later. Default, fast.majority— only data acknowledged by a majority, so it will not be rolled back.linearizable— the strongest, reflecting all prior majority-acknowledged writes; slowest.
The trap is reading from secondaries, which many drivers offer for scaling reads: secondaries
lag the primary, so you can read stale data — write something, read it back from a secondary,
and not see it. This is eventual consistency, and it is a real behaviour of a MongoDB cluster
that a single PostgreSQL primary does not have. If you read your own writes, read from the primary
or use majority.
Isolation, honestly compared
PostgreSQL gave you four isolation levels (module 6). MongoDB's model is different:
- Outside a transaction, there is no snapshot isolation across operations — each read sees the latest committed data at the moment it runs, so two reads in a row can differ.
- Inside a multi-document transaction, you get snapshot isolation — a consistent view for
the transaction's duration, comparable to PostgreSQL's
REPEATABLE READ.
So the concurrency anomalies from module 6 still exist and still need thought. $inc and the other
atomic operators handle the single-document lost update; for cross-document consistency you need a
transaction, and therefore a replica set. The anomalies did not disappear because the database is
document-shaped — the tools to prevent them just look different.
The practical rules
Model to make single-document atomicity enough. Embed what changes together. This is the first and best answer, and it is a modelling decision, not a transactions one.
Use $inc, $push, $pull and the atomic operators rather than read-modify-write, to avoid
lost updates on a single document for free.
Use a multi-document transaction when data genuinely spans documents and must change together — and know it needs a replica set.
Set w: "majority" for writes you cannot lose. The default can lose a write on failover.
Read from the primary, or with majority, if you must read your own writes. Secondaries lag.
If you are reaching for transactions constantly, reconsider the model — or reconsider whether this was a relational problem all along.
Check your work
The always-available guarantee. A single-document write is atomic across all its fields and arrays.
Why this rewards embedding. Data embedded in one document changes atomically with no transaction.
Why $inc is safe against the lost update. It increments on the server atomically, with no
read-modify-write window.
Since when MongoDB has multi-document transactions, and the condition. Since 4.0, and they require a replica set or sharded cluster — a standalone cannot.
The error on a standalone. "does not support retryable writes" — a topology problem, not a code bug.
MongoDB's stance on transactions. They cost, and needing them often signals the data should have been embedded.
What write concern controls, and the three settings. How many nodes confirm — w: 1
(primary only, fast, loseable on failover), w: "majority" (survives failover), j: true (on
disk).
The risk of the default w: 1. A write can be lost on failover.
What reading from a secondary risks. Stale data — eventual consistency; you may not see your own write.
MongoDB's isolation model. No cross-operation snapshot outside a transaction; snapshot
isolation inside one (like REPEATABLE READ).
The first and best answer to a would-be multi-document transaction. Model the data so single-document atomicity is enough.
Practice
- Do a single-document
updateOnethat changes two fields and a$push. Confirm it is all-or-nothing. - Use
$incto implement a counter and reason about why two concurrent increments cannot lose one. - Run the multi-document transaction against a standalone
mongodand read the error. - Start MongoDB as a single-node replica set (
--replSet, thenrs.initiate()) and run the same transaction successfully. - In that transaction, make the second statement fail and confirm the first rolled back.
- Insert with
w: 1and withw: "majority"and time both on a replica set. - Write a document, then read it from a secondary (
readPreference: secondary) and observe whether you always see it. - Redesign a two-document update from your data so it becomes a single-document atomic write.
- Take the module 6 lost-update scenario and reproduce it in MongoDB with read-modify-write, then
fix it with
$inc. - Decide, for an application you know, which writes need
w: "majority"and which can toleratew: 1.
Official documentation
- MongoDB — Transactions — Multi-document ACID transactions and the replica-set requirement.
- MongoDB — Atomicity and transactions — Single-document atomicity, the foundation.
- MongoDB — Write concern —
w,j, and the durability trade. - MongoDB — Read concern —
local,majority,linearizable. - MongoDB — Read preference — Reading from secondaries and the staleness it introduces.
- MongoDB — Convert a standalone to a replica set — So you can test transactions locally.
Next: where documents genuinely win, and where it was fashion.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship