Which MongoDB write operations are atomic without opening an explicit transaction?
answer
- Ask what the unit of atomicity is
- It is not the command, it is smaller
- Many operators, one document, one write
- updateMany is atomic per document only
- Model co-changing data into one document
basics
~20 sEvery write that touches a single document is atomic in MongoDB, no matter how many fields, embedded documents or array elements it changes. Atomicity stops at the document boundary: a write spanning several documents needs an explicit transaction to be all-or-nothing.
solid answer
~40 sMongoDB guarantees atomicity **per document**. `insertOne`, `updateOne`, `replaceOne`, `deleteOne`, `findOneAndUpdate` and an upsert all apply completely or not at all, even when the update contains several operators (`$set`, `$inc`, `$push`) reaching into embedded documents and arrays — no concurrent reader ever sees half of it. What is *not* atomic is the batch: `updateMany`, `deleteMany`, `insertMany` and `bulkWrite` are atomic per document, so another client can observe some documents changed and others not, and an error partway through leaves the earlier documents changed. That is exactly why MongoDB modelling pushes data that must change together into one document: the free guarantee covers the whole aggregate. When the invariant genuinely spans documents or collections, you open a multi-document transaction and pay for it.
code
javascript · 9 lines// Atomic: one document, four operators, applied together
db.carts.updateOne(
{ _id: cartId },
{
$inc: { itemCount: 1, total: 19.99 },
$push: { items: { sku: "A-12", qty: 1, price: 19.99 } },
$set: { updatedAt: new Date() }
}
);go deeper
Recall the headline: a write to one document is all-or-nothing, including when it changes several fields at once. Know that updateMany does not roll back if it fails halfway.
Explain that the document, not the command, is the atomicity boundary, and that WiredTiger locks per document. Show why server-side $inc is safe where a read-modify-write in application code is not.
Show the modelling consequence in production: identify the invariants that must hold, keep them inside one document where you can, and justify a transaction only where the invariant genuinely spans documents.
Own the trade at system level — every invariant pushed across document boundaries becomes coordination cost and an operational failure mode, while embedding buys atomicity at the price of document growth and the 16 MB ceiling.
## What "atomic" means here An operation is atomic when no other client can observe it half-applied and when a failure leaves no partial effect. It is a separate promise from durability (which write concern controls) and from isolation across *many* operations (which transactions control). MongoDB's core atomicity promise is scoped to exactly one thing: a single document. ## Every single-document write is atomic, for free Any write command that resolves to one document is atomic: - `insertOne`, `deleteOne`, `replaceOne` - `updateOne`, including an upsert that creates the document - `findOneAndUpdate`, `findOneAndReplace`, `findOneAndDelete` The guarantee does not weaken as the update grows. An update document combining `$set` on a top-level field, `$inc` on a counter, `$push` on an array and `$unset` on a nested field is still one atomic write — all four operators are applied together or none are. The same holds for deeply nested paths (`items.3.qty`) and for positional updates. WiredTiger takes a document-level write lock and the change is written as one unit, so a reader either sees the previous version of the document or the fully updated one. This is what makes read-modify-write in the *server* safe. `{ $inc: { views: 1 } }` is correct under any level of concurrency; reading `views` into application code, adding one, and writing it back is not, because two clients can interleave between the read and the write. `findOneAndUpdate` exists precisely so that you can perform a conditional mutation and get the document back in one atomic step. ## Where atomicity stops The boundary is the document, not the command. `updateMany({status: "pending"}, {$set: {status: "sent"}})` matching 500 documents performs 500 atomic writes, not one atomic write of 500 documents. Consequences: - A concurrent `find()` can return a mixture: some documents already updated, some not. - If the operation fails partway — a validation failure, a stepdown, a duplicate key — the documents already modified stay modified. Nothing rolls back. - The same applies to `insertMany`, `deleteMany` and `bulkWrite`, and to any sequence of separate calls your application makes. There is also no snapshot for plain reads. A `find()` cursor that scans a large collection while writes are happening can return some documents in their pre-update state and others post-update; it does not see a point-in-time view of the collection unless it runs inside a transaction with an appropriate read concern. The old `$isolated` operator that once offered a weak form of this was removed and does not exist in current versions. ## Why this shapes the data model Because the free guarantee covers one document, MongoDB modelling asks: *what has to change together?* If an order and its line items must always agree, keeping the line items embedded in the order document turns a multi-row transaction into a single atomic update. If a counter must match a list, keep both in the same document and update them in one command. Teams that carry a relational model over unchanged — one collection per entity, references everywhere — end up reaching for transactions on the hot path and paying for coordination they could have modelled away. The pressure runs the other way too: unbounded arrays grow the document, and a document has a hard 16 MB BSON size limit, so "embed everything" is not a free win either. The atomicity guarantee is one input to that trade, not the whole of it. ## When you genuinely need more Multi-document transactions exist for the cases that cannot be modelled into one document — moving a balance between two accounts held in different documents, writing a record and its index-like companion document in another collection, or maintaining a cross-collection invariant during a migration. They are available on replica sets and on sharded clusters, they give snapshot isolation across all their reads and all-or-nothing semantics across all their writes, and they cost real coordination: a held snapshot, a time limit, fast-failing write conflicts, and two-phase commit if more than one shard is involved. ## What single-document atomicity does not give you - **Not durability.** Atomic says "all or nothing"; it does not say the write survives a primary failure. That is write concern. - **Not cross-document isolation.** Two atomic single-document writes issued back to back can be observed between. - **Not application-level idempotency.** If your client times out and retries, the *server* may still have applied the first attempt; retryable writes handle the transport-level case, but an operation that is not naturally idempotent still needs thought. In an interview, the expected answer is short and precise: the document is the unit of atomicity, everything wider is a per-document guarantee only, and the first design move is to put co-changing data in one document rather than to reach for a transaction.
- Why is { $inc: { views: 1 } } safe under concurrency when reading the value and writing it back is not?`$inc` is evaluated by the server inside the single atomic write on that document, so concurrent increments serialize and each one is applied to the value in place. Reading into application code and writing back opens a window between the read and the write in which another client can commit a change, which the write-back then overwrites — the classic lost update. `findOneAndUpdate` gives you the same atomicity when you also need the document back.
- Does a plain find() that scans a collection give you a consistent point-in-time view?No. A normal cursor has no snapshot across documents: while it iterates, concurrent writes can land, so it may return some documents in their old state and others in their new state, and a document moved by an update can in principle be missed or seen twice. If you need a stable view across many documents you need a transaction with a suitable read concern, or an aggregation designed to tolerate the churn.
- If atomicity is per document, what does the 16 MB BSON limit have to do with the design?It caps how much you can pull into the atomic unit. Embedding co-changing data buys free atomicity, but an unbounded array — every event, every comment — eventually approaches the 16 MB document limit and makes every update rewrite a large document. So embedding is right for bounded, co-changing data and wrong for unbounded growth, where you reference instead and accept the coordination cost.
A single document is like a sealed envelope: you can rewrite as many pages inside it as you like and the reader still only ever sees the old envelope or the new one — but sending five envelopes is five separate deliveries.
saying these in an interview costs you the question
- Claims every write needs a transaction to be atomic
- Thinks updateMany rolls back on partial failure
- Says each update operator is applied as a separate write
- Believes a plain find() sees a consistent snapshot
- Confuses atomicity with write concern durability