R
Rishtaara
MongoDB Fundamentals
Lesson 17 of 40Article14 min

FindAndModify Family

Find-and-modify operations atomically find a document and apply a change in a single step. This prevents race conditions when multiple clients compete for the same document — counters, job queues, and inventory locks.

Why find-and-modify?

Find-and-modify operations atomically find a document and apply a change in a single step. This prevents race conditions when multiple clients compete for the same document — counters, job queues, and inventory locks.

Real-life example: findOneAndUpdate is like a ticket counter that hands you the next number AND increments the counter in one motion — no two people get the same number.

  • findOneAndUpdate — find and apply update operators.
  • findOneAndReplace — find and replace entire document.
  • findOneAndDelete — find and remove.
  • Legacy findAndModify — deprecated, same family.

findOneAndUpdate and findOneAndReplace

findOneAndUpdate applies update operators. findOneAndReplace swaps the whole document. Both support upsert and returnDocument to control what gets returned.

Real-life example: returnDocument: 'after' is like seeing the receipt after the cashier updates your order — not the old version.

findOneAndUpdate — atomic counter
use rishtaara

// Auto-increment order ID (atomic)
const counter = db.counters.findOneAndUpdate(
  { _id: "order_seq" },
  { $inc: { seq: 1 } },
  {
    upsert: true,
    returnDocument: "after"   // "before" (default) or "after"
  }
)
// counter.seq is the new value — use for orderNumber

// Claim next pending job from a queue
const job = db.jobs.findOneAndUpdate(
  { status: "pending" },
  {
    $set: { status: "processing", claimedAt: new Date() }
  },
  {
    sort: { priority: -1, createdAt: 1 },
    returnDocument: "after"
  }
)
findOneAndReplace
// Replace entire config document
db.settings.findOneAndReplace(
  { key: "feature_flags" },
  {
    key: "feature_flags",
    darkMode: true,
    betaSearch: false,
    updatedAt: new Date()
  },
  { upsert: true, returnDocument: "after" }
)

// Prefer findOneAndUpdate with $set for partial changes
db.settings.findOneAndUpdate(
  { key: "feature_flags" },
  { $set: { betaSearch: true, updatedAt: new Date() } },
  { returnDocument: "after" }
)

findOneAndDelete and legacy findAndModify

findOneAndDelete atomically finds and removes a document — useful for queue consumers and pop-from-stack patterns. The legacy findAndModify command still works but prefer the modern methods.

Real-life example: findOneAndDelete is like taking the top item off a stack — you get the item and it is removed in one action.

returnDocument: 'before' returns the document as it was before the change. returnDocument: 'after' returns the updated document. Default is 'before'.
findOneAndDelete
// Pop oldest unprocessed notification
const notification = db.notifications.findOneAndDelete(
  { userId: ObjectId("665a1b2c3d4e5f6789012345"), read: false },
  { sort: { createdAt: 1 } }
)
// Returns the deleted document (before deletion)

// Process and remove expired session
const session = db.sessions.findOneAndDelete({
  token: "abc123xyz",
  expiresAt: { $gt: new Date() }
})

if (!session) {
  print("Session invalid or expired")
}
Legacy findAndModify vs modern API
// ❌ Legacy command (deprecated)
db.runCommand({
  findAndModify: "counters",
  query: { _id: "invoice_seq" },
  update: { $inc: { seq: 1 } },
  new: true,       // old name for returnDocument: "after"
  upsert: true
})

// ✅ Modern equivalent
db.counters.findOneAndUpdate(
  { _id: "invoice_seq" },
  { $inc: { seq: 1 } },
  { returnDocument: "after", upsert: true }
)

returnDocument — before vs after

Choose returnDocument based on what your application logic needs. Counters usually want 'after'. Audit logs may want 'before' to record the previous state.

Real-life example: 'before' is like a before-photo at the gym. 'after' is the progress photo — most apps want to show the new state.

  • Use sort with findOneAnd* when multiple documents match — controls which one is picked.
  • findOneAnd* methods are atomic on a single document — safe for concurrent workers.
  • In Mongoose: findOneAndUpdate maps to the same server command with { new: true } for 'after'.
Comparing before and after
db.products.insertOne({ name: "Test Item", stock: 100, price: 500 })

// BEFORE (default) — returns old document
const before = db.products.findOneAndUpdate(
  { name: "Test Item" },
  { $inc: { stock: -5 } },
  { returnDocument: "before" }
)
// before.stock === 100

// AFTER — returns updated document
const after = db.products.findOneAndUpdate(
  { name: "Test Item" },
  { $inc: { stock: -5 } },
  { returnDocument: "after" }
)
// after.stock === 95