MongoDB / Basic: document foundations / Lesson 6
Inserts, updates, deletes and atomic filters
Make writes express their own business preconditions. Distinguish update operators, replacement, upsert, acknowledgment and retry identity.
On this page
Lesson goals
- Use scoped CRUD and conditional arithmetic updates.
- Explain document atomicity and duplicate-request semantics.
One document is the default atomic boundary
A MongoDB write modifying one document is atomic even when several fields change. This does not mean updateMany is atomic across every matching document. A preliminary application read followed by an unconditional absolute replacement can overwrite a concurrent change. Put expected state or version in the update filter, and use operators such as $inc when arithmetic is intended. The application must inspect matched and modified outcomes under an acknowledged write concern rather than treating a returned response as proof of the desired business event.
Keep the sufficient-stock rule inside the update
Assume item 4 has quantity nine in the isolated synthetic inventory. The command subtracts three only if quantity is at least three, giving quantity six when one document matches. If quantity is two, matchedCount is zero and no reservation succeeds. The example’s expected result assumes acknowledged success and a known fixture; write concern and network uncertainty are discussed later. A missing item also produces no match, so distinguish it from insufficient stock only when the API contract requires that additional decision.
db.getSiblingDB('quay_lab').inventory.updateOne(
{ _id: 4, quantity: { $gte: 3 } },
{ $inc: { quantity: -3 } }
);
Expected with quantity initially 9: matchedCount = 1, modifiedCount = 1; quantity becomes 6.
The stock predicate and arithmetic change are evaluated as one single-document update.
Replacement discards omitted ordinary fields
updateOne with $set changes selected fields, while replaceOne replaces the document body subject to _id rules; omitted fields are not preserved automatically. Upsert inserts a document if the filter finds none, which is not the same as “ignore every error.” Concurrent upserts need a suitable unique identity to avoid duplicate logical records. A unique index protects the chosen key but does not make an incremented account balance idempotent for a retried payment event. Define reference-row identity and event identity separately.
Use optimistic state checks deliberately
Assume booking 101 is pending with version two. The filter requires that exact version and state before marking it dispatched and incrementing version to three. A competing writer that already advanced the version makes this attempt match nothing. The application should re-read and reconsider the decision rather than removing the version check. This is optimistic concurrency, not a transaction spanning external systems. An outbox or another reviewed integration design is needed when dispatch also triggers a message or downstream effect.
db.getSiblingDB('quay_lab').bookings.updateOne(
{ _id: 101, state: 'pending', version: 2 },
{ $set: { state: 'dispatched' }, $inc: { version: 1 } }
);
Expected after one successful matching update: state dispatched and version 3. A stale version returns no match.
Bulk completion is not rollback of earlier writes
Ordered bulk writes stop at a qualifying error after earlier successful operations have already taken effect. Unordered bulk work can continue with other operations and does not promise execution order. Neither behavior is an automatic all-or-nothing batch transaction. DELETE filters deserve the same exact scope review as UPDATE, especially tenant boundaries and empty filters. Network errors can leave an uncertain outcome; use driver-supported retry behavior only under its documented constraints and persist idempotency keys for business events that must not apply twice.
Common errors and fixes
A reservation overwrites another writer’s quantity
- Why it happens
- An absolute value came from a stale application read.
- How to fix it
- Use a conditional operator update or reviewed version filter.
- Prevent it next time
- Put invariants and expected state inside the atomic filter.
Fields disappear after replaceOne
- Why it happens
- Replacement was mistaken for a partial $set update.
- How to fix it
- Use update operators or supply the complete intended replacement body.
- Prevent it next time
- Review omitted-field behavior before choosing a write method.
A failed ordered bulk leaves partial effects
- Why it happens
- Stop-on-error was assumed to roll back earlier operations.
- How to fix it
- Reconcile known outcomes and use a transaction only when its supported contract is required.
- Prevent it next time
- Test partial failures and idempotent recovery paths.
Check your understanding
1. Inventory is two and the request needs three. What is the correct next decision after the conditional update?
Show worked solution
No document matches and no reservation succeeds. Return the defined insufficient-stock or missing-item outcome after any necessary distinction, without weakening the condition. A subsequent smaller request may succeed through the same atomic pattern. Do not turn a zero-match response into an unconditional decrement, because that discards the invariant the filter was designed to protect.
2. A payment request is retried after a lost response. Why is upsert on accountId alone insufficient?
Show worked solution
The account key identifies the account, not the individual payment event, so another increment can apply the same payment twice. Store a unique request or payment identifier with its recorded result and coordinate the effect under the required transaction or single-document model. On duplicate event identity, return the known outcome rather than applying a second financial change.
Key takeaways
- Document atomicity does not imply batch or external-system atomicity.
- Conditional filters, acknowledgment and event identity belong in write design.