MongoDB CRUD uses documents in collections: InsertOne/InsertMany (create), find/findOne (read), updateOne/updateMany with operators (update), deleteOne/deleteMany (delete).
The _id field is auto-generated as an ObjectId — capture it immediately after insert to avoid extra queries.
Update operators ($set, $inc, $push) prevent accidental document replacement; never pass a plain object as the update argument.
find() returns a cursor, not an array — always chain .toArray() or iterate; use .limit() on open-ended queries to protect production.
Soft-delete (isDeleted flag + deletedAt timestamp) is the production standard; physical deletion reserved for cleanup or GDPR erasure.
Plain-English First
Imagine MongoDB is a giant filing cabinet where each drawer is a 'collection' and each folder inside is a 'document'. CRUD is just the four things you'd ever do to that cabinet: drop in a new folder (Create), read what's inside one (Read), scribble changes on it (Update), or shred it (Delete). That's it — every database operation in existence boils down to these four actions.
Every application that stores data — whether it's a social media feed, an e-commerce cart, or a hospital records system — needs to talk to a database. MongoDB has become the go-to choice for teams building flexible, fast-moving products because it stores data as JSON-like documents instead of rigid rows and columns. Knowing how to speak its language isn't optional; it's the difference between an app that ships and one that stalls.
Create — Inserting Documents the Right Way
Inserting data sounds trivial until you're doing it wrong in production. MongoDB gives you two insertion methods: insertOne() for a single document and insertMany() for a batch. The key insight most tutorials skip is what MongoDB hands back after an insert — an acknowledgement object containing the auto-generated _id. That _id is a 12-byte ObjectId, globally unique by design, and you should be capturing it in your application logic rather than ignoring it.
Why does this matter? Because in a typical e-commerce flow you insert an order document, then immediately need that order's _id to create a shipment record that references it. Ignoring the return value forces a second round-trip to the database just to find what MongoDB already told you.
insertMany() is more nuanced. By default it's ordered, meaning if document number 3 of 10 fails validation, documents 1 and 2 are already committed and 4-10 are abandoned. Passing the option { ordered: false } lets MongoDB push through all valid documents and collect errors at the end — a much better pattern for bulk imports where one bad record shouldn't kill the whole batch.
insertOrders.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Connect to a local MongoDB instance using the official Node.js driverconst { MongoClient, ObjectId } = require('mongodb');
asyncfunctioninsertOrders() {
const client = new MongoClient('mongodb://localhost:27017');try {
await client.connect();
const db = client.db('storefront'); // target database
const ordersCollection = db.collection('orders'); // target collection// --- insertOne: single document ---// MongoDB auto-generates _id if you don't provide oneconst singleInsertResult = await ordersCollection.insertOne({
customerId: 'cust_8821',
items: [
{ productSku: 'SHOE-RED-42', quantity: 1, unitPrice: 79.99 },
{ productSku: 'LACE-BLK', quantity: 2, unitPrice: 3.50 }
],
orderStatus: 'pending',
createdAt: new Date() // always store dates as Date objects, not strings
});
// insertedId is the ObjectId MongoDB assigned — save this, don't query for it again
console.log('New order ID:', singleInsertResult.insertedId);
// --- insertMany: bulk insert with ordered:false so one bad doc doesn't abort the rest ---const bulkOrders = [
{
customerId: 'cust_1103',
items: [{ productSku: 'HAT-GRN-M', quantity: 1, unitPrice: 24.00 }],
orderStatus: 'pending',
createdAt: newDate()
},
{
customerId: 'cust_4477',
items: [{ productSku: 'BELT-BRN-L', quantity: 1, unitPrice: 34.50 }],
orderStatus: 'pending',
createdAt: newDate()
}
];
const bulkInsertResult = await ordersCollection.insertMany(bulkOrders, {
ordered: false // continue inserting even if one document fails validation
});
// insertedCount tells you how many actually landed
console.log('Orders inserted:', bulkInsertResult.insertedCount);
console.log('Inserted IDs:', bulkInsertResult.insertedIds);
} finally {
await client.close(); // always close the connection
}
}
insertOrders().catch(console.error);
After insertOne(), grab result.insertedId right there in the same function. Every extra database round-trip to 'find' the document you just created is wasted latency you already paid for.
Production Insight
Inserting into a shard cluster with a monotonically increasing _id (like a timestamp) can cause hotspotting on one shard. Use a random or hash-based shard key.
Always handle duplicate key errors (E11000) in your code — they're not bugs, they're design invariants.
Rule: choose _id generation strategy based on your write pattern.
Key Takeaway
Capture insertedId immediately after insertOne().
Use {ordered:false} for bulk inserts when one failure shouldn't abort the batch.
Never ignore the result object from insert operations.
Read — Querying Documents Without Killing Your Database
Reading data is where most MongoDB performance problems are born. find() returns a cursor, not an array — meaning MongoDB streams results lazily rather than loading everything into memory at once. This is a feature, not a quirk, and it matters the moment your collection grows past a few thousand documents.
The filter argument is where the real power lives. MongoDB's query language is composable: you can filter by exact match, range ($gte, $lte), array membership ($in), logical operators ($and, $or), and even run regex searches — all within a single query object. But power without discipline is dangerous. Running find({}) on a million-document collection with no limit() is how you bring a production server to its knees.
Projection is the query-level equivalent of SELECT in SQL — it tells MongoDB which fields to return. Always use it. Fetching a 40-field customer document when your UI only needs name and email wastes bandwidth, serialization time, and memory on both sides of the wire.
Indexes are what make reads fast, but that's a separate topic. The habit to build now is: every field you filter or sort on should eventually have an index behind it. Use explain('executionStats') on any query you care about to see whether MongoDB is doing a full collection scan (bad) or an index scan (good).
queryOrders.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
const { MongoClient } = require('mongodb');
asyncfunctionqueryOrders() {
const client = new MongoClient('mongodb://localhost:27017');try {
await client.connect();
const ordersCollection = client.db('storefront').collection('orders');
// --- findOne: grab the first match, returns a plain object (not a cursor) ---const latestPendingOrder = await ordersCollection.findOne(
{ orderStatus: 'pending' }, // filter: only pending orders
{
projection: { customerId: 1, items: 1, createdAt: 1, _id: 0 }, // only return these fields
sort: { createdAt: -1 } // newest first
}
);
console.log('Latest pending order:', latestPendingOrder);
// --- find with range filter: orders created in the last 7 days ---const sevenDaysAgo = newDate(Date.now() - 7 * 24 * 60 * 60 * 1000);
const recentOrders = await ordersCollection
.find(
{
createdAt: { $gte: sevenDaysAgo }, // $gte = greater than or equal to
orderStatus: { $in: ['pending', 'processing'] } // $in matches any value in the array
},
{
projection: { customerId: 1, orderStatus: 1, createdAt: 1 }
}
)
.sort({ createdAt: -1 })
.limit(25) // ALWAYS limit open-ended queries in production
.toArray(); // materialise the cursor into an array
console.log(`Found ${recentOrders.length} recent orders`);
recentOrders.forEach(order => {
console.log(` ${order.customerId} — ${order.orderStatus} — ${order.createdAt.toISOString()}`);
});
// --- countDocuments: how many total pending orders exist? ---// Use countDocuments() NOT count() — count() is deprecated and ignores filters in some edge casesconst pendingTotal = await ordersCollection.countDocuments({ orderStatus: 'pending' });
console.log('Total pending orders:', pendingTotal);
} finally {
await client.close();
}
}
queryOrders().catch(console.error);
find() returns a Cursor object. Logging it directly prints cursor metadata, not your documents. Always chain .toArray() or iterate with for await...of. Forgetting this is a rite of passage — and a runtime bug that's surprisingly hard to spot.
Production Insight
A find() with no .limit() can kill your application and database under load. Always paginate or limit.
Missing indexes on filter fields lead to full collection scans; use explain() to verify.
Rule: every query filter and sort field that runs more than once a day needs an index.
Key Takeaway
find() returns a lazy Cursor — materialise with .toArray() or iterate.
Always use projection to fetch only needed fields.
Add .limit() to every open-ended query before it hits production traffic.
Update — Changing Data Without Replacing It
Updating is where MongoDB beginners most often shoot themselves. The critical rule: always use an update operator like $set, $inc, or $push. If you pass a plain document as the second argument to updateOne(), MongoDB treats it as a full replacement and wipes every field not in your update object. That's a legal operation, but it's almost never what you want.
MongoDB's update operators are surgical. $set modifies only the fields you name, leaving everything else untouched. $inc atomically increments a number — perfect for view counters or inventory tracking without a read-modify-write cycle. $push appends to an array, and $pull removes from one. $unset deletes a field entirely.
The upsert option is powerful but underused. Setting { upsert: true } tells MongoDB: if the filter matches something, update it; if nothing matches, create a new document. This collapses a common 'find-then-insert-or-update' pattern into a single atomic operation — no race conditions, no extra round-trips.
For bulk changes, updateMany() applies your update to every document matching the filter. Just make sure your filter is tight. Running updateMany({}, { $set: { archived: true } }) marks every single document in the collection as archived — no confirmation prompt, no undo.
updateOrders.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const { MongoClient, ObjectId } = require('mongodb');
asyncfunctionupdateOrders() {
const client = new MongoClient('mongodb://localhost:27017');try {
await client.connect();
const ordersCollection = client.db('storefront').collection('orders');
// --- updateOne with $set: change a single order's status ---// ALWAYS use $set — without it, your whole document gets replacedconst targetOrderId = newObjectId('64f3a2b1c9e77f001a3d8e12');
const statusUpdateResult = await ordersCollection.updateOne(
{ _id: targetOrderId }, // filter: match by exact _id
{
$set: {
orderStatus: 'dispatched',
dispatchedAt: new Date() // add a new field on the fly — no schema migration needed
}
}
);
console.log('Documents matched:', statusUpdateResult.matchedCount); // how many matched the filter
console.log('Documents modified:', statusUpdateResult.modifiedCount); // how many were actually changed// --- $inc: atomically decrement stock without a read-modify-write ---// This is safe under concurrent writes; plain read+write is NOTconst inventoryCollection = client.db('storefront').collection('inventory');
await inventoryCollection.updateOne(
{ productSku: 'SHOE-RED-42' },
{ $inc: { stockCount: -1 } } // subtract 1 from stockCount atomically
);
// --- upsert: create a shipment record if it doesn't exist, update if it does ---const shipmentResult = await client.db('storefront').collection('shipments').updateOne(
{ orderId: targetOrderId }, // filter: does a shipment for this order exist?
{
$set: {
orderId: targetOrderId,
carrier: 'FastFreight',
trackingNumber: 'FF-993821-XZ',
estimatedDelivery: newDate('2024-09-05')
}
},
{ upsert: true } // create it if it doesn't exist
);
// upsertedId is non-null only when a new document was createdif (shipmentResult.upsertedId) {
console.log('Shipment record created with ID:', shipmentResult.upsertedId);
} else {
console.log('Existing shipment record updated');
}
// --- updateMany: mark all orders older than 30 days as 'archived' ---const thirtyDaysAgo = newDate(Date.now() - 30 * 24 * 60 * 60 * 1000);
const archiveResult = await ordersCollection.updateMany(
{ createdAt: { $lt: thirtyDaysAgo }, orderStatus: 'dispatched' }, // tight filter!
{ $set: { orderStatus: 'archived', archivedAt: newDate() } }
);
console.log(`Archived ${archiveResult.modifiedCount} old orders`);
} finally {
await client.close();
}
}
updateOrders().catch(console.error);
Output
Documents matched: 1
Documents modified: 1
Shipment record created with ID: 64f3a2b1c9e77f001a3d9f01
Archived 0 old orders
Watch Out: Update Without $set Replaces the Document
updateOne({ _id: id }, { orderStatus: 'dispatched' }) doesn't add a field — it replaces the entire document with { orderStatus: 'dispatched' }. You'll lose every other field silently. Always wrap your changes in { $set: { ... } }.
Production Insight
Forgetting $set is the leading cause of accidental data loss in MongoDB. It's silent — no error, just missing data.
$inc is atomic; separate read-modify-write is not safe under concurrency.
Rule: if you need to update multiple documents with different values, consider bulkWrite with multiple updateOne statements.
Key Takeaway
Always wrap update fields in an operator: $set, $inc, $push, etc.
Use upsert:true to avoid separate find-then-insert race conditions.
Check matchedCount and modifiedCount to confirm the update had intended effect.
Delete — Removing Data Safely and Intentionally
Deletion in MongoDB is permanent and instantaneous. There's no recycle bin, no soft-delete built in, and no ROLLBACK. This is why production teams almost universally implement soft-deletes — adding a deletedAt timestamp field and filtering it out of queries — rather than physically removing documents. Physical deletion is reserved for true cleanup jobs like purging GDPR-expired data or clearing test fixtures.
MongoDB gives you deleteOne() for surgical removal of a single document and deleteMany() for bulk removal. The same golden rule from updates applies: if your filter is too broad, you will delete more than you intended. Always test your filter with a find() call first — confirm the count and spot-check a few returned documents before converting it to a deleteMany().
findOneAndDelete() is the atomic 'grab it and kill it' operation. It deletes the document and returns it to your application in a single server-side operation. This is exactly what you need for job queue patterns where a worker claims a task — using separate find() then deleteOne() calls creates a race condition where two workers could claim the same job.
Never run deleteMany({}) in production without a filter. There's no faster way to have a very bad day.
deleteOrders.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
const { MongoClient, ObjectId } = require('mongodb');
asyncfunctiondeleteOrders() {
const client = new MongoClient('mongodb://localhost:27017');try {
await client.connect();
const db = client.db('storefront');
const ordersCollection = db.collection('orders');
// --- PATTERN 1: Soft delete (recommended for business data) ---// Don't physically remove — mark as deleted so you keep the audit trailconst orderToSoftDelete = newObjectId('64f3a2b1c9e77f001a3d8e13');
await ordersCollection.updateOne(
{ _id: orderToSoftDelete },
{
$set: {
isDeleted: true,
deletedAt: new Date() // lets you audit WHEN it was deleted and query "deleted in last 30 days"
}
}
);
console.log('Order soft-deleted (record preserved for audit)');
// --- PATTERN 2: Hard delete with deleteOne ---// Appropriate for test data, temp records, or GDPR erasure requestsconst tempOrderId = newObjectId('64f3a2b1c9e77f001a3d8e14');
const hardDeleteResult = await ordersCollection.deleteOne({ _id: tempOrderId });
console.log('Hard-deleted document count:', hardDeleteResult.deletedCount);
// deletedCount will be 0 if the _id didn't exist — not an error, just a miss// --- PATTERN 3: findOneAndDelete — atomic claim-and-remove for job queues ---const jobsCollection = db.collection('pendingEmailJobs');
// Grab the highest-priority job AND remove it in one atomic step// If two workers call this simultaneously, only one gets the documentconst claimedJob = await jobsCollection.findOneAndDelete(
{ status: 'queued' },
{
sort: { priority: -1, queuedAt: 1 }, // highest priority, then FIFO
returnDocument: 'before' // return the document as it was before deletion
}
);
if (claimedJob) {
console.log('Worker claimed job:', claimedJob.jobId, '| Recipient:', claimedJob.recipientEmail);
} else {
console.log('No jobs in queue right now');
}
// --- PATTERN 4: deleteMany for bulk cleanup (always preview first!) ---// Step 1: preview — what would get deleted?const toDeleteCount = await ordersCollection.countDocuments({
isDeleted: true,
deletedAt: { $lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) } // older than 90 days
});
console.log(`About to permanently purge ${toDeleteCount} soft-deleted orders...`);
// Step 2: execute only when you're sureconst purgeResult = await ordersCollection.deleteMany({
isDeleted: true,
deletedAt: { $lt: newDate(Date.now() - 90 * 24 * 60 * 60 * 1000) }
});
console.log('Purged:', purgeResult.deletedCount, 'old orders');
} finally {
await client.close();
}
}
deleteOrders().catch(console.error);
Output
Order soft-deleted (record preserved for audit)
Hard-deleted document count: 1
No jobs in queue right now
About to permanently purge 0 soft-deleted orders...
Purged: 0 old orders
Pro Tip: Preview Before deleteMany
Run countDocuments() with the exact same filter before any deleteMany() call. It's a two-second habit that has saved entire collections from accidental wipes. Treat deleteMany like a controlled demolition — measure twice, delete once.
Production Insight
Physical deletes are permanent; there is no rollback without a backup. Soft-deletes give you a recovery window.
findOneAndDelete is the only safe way to implement job queues — separate find and delete creates races.
Rule: preview your filter with countDocuments before any deleteMany.
Key Takeaway
Use soft-deletes for business-critical data.
Use findOneAndDelete for atomic claim-and-remove.
Always test the filter on a find before running deleteMany.
Write Concerns, Error Handling, and Retry Logic
Production applications need to decide how durable their writes are. MongoDB's writeConcern setting controls the level of acknowledgment: w:1 (acknowledge from primary only), w:majority (ack from replica set majority), or j:true (journaled). Each level trades latency for durability. If you absolutely cannot lose a write — say a payment confirmation — use w:majority and j:true. For logs or transient data, w:1 is fine.
Errors happen. Duplicate key errors (E11000) are thrown when a unique index constraint is violated. Your code must catch them. Network timeouts and writeConcern timeouts are distinct: the former means the driver couldn't reach the server, the latter means the server couldn't gather enough acknowledgments in time. Both require retry logic. The MongoDB driver provides built-in retryable writes for network errors, but not for writeConcern errors. You'll need to implement exponential backoff yourself.
A robust write pattern: attempt the write, catch errors, inspect the error label to distinguish transient from permanent, retry transient errors with backoff, and log permanent errors for later investigation. Never swallow errors — a failed write that goes unnoticed becomes a silent data loss.
writeWithRetry.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { MongoClient } = require('mongodb');
asyncfunctioninsertWithRetry(collection, doc, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Set high writeConcern for critical dataconst result = await collection.insertOne(doc, {
writeConcern: { w: 'majority', j: true }
});
return result;
} catch (err) {
// Differentiate transient vs permanentif (err.code === 11000) {
// Duplicate key — permanent, log and abort
console.error('Duplicate key for doc:', JSON.stringify(doc));
throw err; // Re-throw for caller to handle
}
if (err.hasErrorLabel('TransientTransactionError') || err.message.includes('timeout')) {
// Transient — retry with backoffif (attempt === maxRetries) {
console.error('Max retries reached, write failed');
throw err;
}
const delay = Math.pow(2, attempt) * 100; // 200ms, 400ms, 800ms
console.log(`Retrying write, attempt ${attempt + 1} after ${delay}ms`);
awaitnewPromise(resolve => setTimeout(resolve, delay));
} else {
// Other permanent errorthrow err;
}
}
}
}
Beware the Silent WriteConcern Timeout
A writeConcern timeout does not mean the write failed — it means the acknowledged count wasn't confirmed in time. The write may still succeed on the primary. Use retry logic with idempotent operations to avoid duplicate inserts on timeout.
Production Insight
Read-preference and writeConcern decisions affect both consistency and latency; test under production-like load.
Retryable writes from the driver handle only network errors — not writeConcern timeouts or duplicate keys.
Rule: implement your own retry with exponential backoff for custom error types.
Key Takeaway
Choose writeConcern based on data criticality: w:majority for payments, w:1 for logs.
Retry transient errors with backoff; log permanent errors without re-throwing silently.
● Production incidentPOST-MORTEMseverity: high
Accidental Bulk Update Wipes Fields on 10k Orders
Symptom
Orders showed only the field that was updated; all other fields missing.
Assumption
The updateOne call with a plain document would only change the specified field.
Root cause
updateOne with a plain document as second argument replaces the whole document; $set is required for partial update.
Fix
Add $set operator to the update call; restore from backup for affected documents.
Key lesson
Always use $set for partial updates; a single-line omit can cost hours of data recovery.
Write a small script to validate the update with findOne before executing on production.
Production debug guideQuick reference for common MongoDB query failures in production4 entries
Symptom · 01
Query returns zero results even though document exists
→
Fix
Check that _id is compared with correct type (string vs ObjectId). Use typeof filter._id; if string, convert with new ObjectId().
Symptom · 02
find() returns cursor metadata instead of documents
→
Fix
Chain .toArray() or use for await...of. Logging find() without materialisation prints cursor info, not data.
Symptom · 03
Update doesn't change any documents
→
Fix
Check matchedCount vs modifiedCount in result object. If matchedCount is 0, filter misses. If modifiedCount is 0, document already matches intended state.
Symptom · 04
insertMany fails validation on first doc and skips rest
→
Fix
Set {ordered: false} to continue inserting valid documents. Check result.writeErrors for details of failed docs.
★ MongoDB CRUD Quick Debug Cheat SheetThree common production failures and the exact steps to diagnose and fix them
No results when querying by _id−
Immediate action
Check the type of the _id value in your filter
Commands
typeof filter._id
If string, convert: new ObjectId(filter._id)
Fix now
Use ObjectId constructor in the query
Update silently deletes fields+
Immediate action
Inspect the update argument for missing $set operator
Commands
console.log('Update argument:', update);
If it's a plain object, wrap in { $set: update }
Fix now
Restore from backup; add $set to all future updates
passing a plain object as the update argument replaces the whole document silently, which is almost never what you want.
2
find() returns a lazy Cursor, not an array
always chain .toArray() or use for await...of, and always add .limit() to open-ended queries before they hit production traffic.
3
findOneAndDelete() and findOneAndUpdate() aren't just convenience methods
they're the only way to atomically claim-and-act on a document without race conditions between concurrent processes.
4
Soft-deletes (adding isDeleted
true and deletedAt fields) preserve your audit trail and make GDPR compliance practical. Reserve physical deletion for temp data, test fixtures, and scheduled purge jobs.
5
Choose writeConcern based on data criticality
w:majority for payments, w:1 for logs. Write idempotent operations and implement retry logic for transient errors.
Common mistakes to avoid
4 patterns
×
Updating without $set
Symptom
Document silently replaced; all fields except the update values are lost.
Fix
Always wrap your changes in a $set (or $inc, $push) operator. Example: updateOne({ _id: id }, { $set: { status: 'active' } }).
×
Comparing string IDs to ObjectId
Symptom
Queries return zero results with no error because the types don't match.
Fix
Wrap the ID string in new ObjectId('...') before using in filter. Always ensure _id field is compared as ObjectId.
×
Using deprecated count() instead of countDocuments()
Symptom
Inaccurate document counts on sharded clusters, leading to pagination bugs.
Fix
Always use countDocuments(filter) which scans live data and respects filters. Avoid count() entirely.
×
Not using projection in reads
Symptom
Massive network bandwidth waste and slow serialization from fetching all fields.
Fix
Always specify projection in find() and findOne() options. Pro tip: exclude _id if not needed.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01SENIOR
What's the difference between updateOne() and replaceOne() in MongoDB, a...
Q02SENIOR
How would you implement an atomic 'claim a task from a queue' operation ...
Q03SENIOR
If countDocuments() and estimatedDocumentCount() both count documents, w...
Q01 of 03SENIOR
What's the difference between updateOne() and replaceOne() in MongoDB, and when would you deliberately choose replaceOne()?
ANSWER
updateOne modifies specific fields using operators like $set, while replaceOne replaces the entire document with a new one. You'd choose replaceOne when you want to change the document's structure entirely — for example, after a GDPR data erasure request where you need to replace a user document with a minimal anonymised version, keeping only the _id.
Q02 of 03SENIOR
How would you implement an atomic 'claim a task from a queue' operation in MongoDB without creating a race condition between two simultaneous workers?
ANSWER
Use findOneAndDelete with a filter for queued tasks and a sort to pick the highest priority. This atomically removes the document and returns it to the worker. For example: db.jobs.findOneAndDelete({ status: 'queued' }, { sort: { priority: -1, queuedAt: 1 } }). This ensures only one worker gets each job.
Q03 of 03SENIOR
If countDocuments() and estimatedDocumentCount() both count documents, why do they exist separately — and which would you use on a 50-million-document collection for a real-time dashboard?
ANSWER
estimatedDocumentCount uses collection metadata and is very fast but can be slightly stale on sharded clusters. countDocuments actually scans documents (using an index) and respects filters, returning an exact count but much slower. For a real-time dashboard on a 50M collection, use estimatedDocumentCount (which is typically under a millisecond) because the small inaccuracy (usually less than a second old) is negligible compared to the performance cost of scanning 50M documents. Only use countDocuments when you need exact counts with filters.
01
What's the difference between updateOne() and replaceOne() in MongoDB, and when would you deliberately choose replaceOne()?
SENIOR
02
How would you implement an atomic 'claim a task from a queue' operation in MongoDB without creating a race condition between two simultaneous workers?
SENIOR
03
If countDocuments() and estimatedDocumentCount() both count documents, why do they exist separately — and which would you use on a 50-million-document collection for a real-time dashboard?
SENIOR
FAQ · 4 QUESTIONS
Frequently Asked Questions
01
What is the difference between insertOne and insertMany in MongoDB?
insertOne() adds a single document and returns a result with the new document's _id. insertMany() adds an array of documents in one network round-trip, which is dramatically faster for bulk loads. The key option to know is { ordered: false }, which tells MongoDB to continue inserting valid documents even if some fail, rather than aborting the entire batch on the first error.
Was this helpful?
02
Why does my MongoDB updateOne() query delete all my document fields?
You're passing a plain object as the second argument instead of using an update operator. updateOne({ _id: id }, { status: 'active' }) replaces the document entirely. You need updateOne({ _id: id }, { $set: { status: 'active' } }) — the $set operator tells MongoDB to modify only the named fields and leave everything else alone.
Was this helpful?
03
Does MongoDB have transactions like SQL databases?
Yes — since version 4.0, MongoDB supports multi-document ACID transactions. For single-document operations, MongoDB has always been atomic. Multi-document transactions are available on replica sets and sharded clusters, but they come with a performance cost. The best practice is to model your data so that most operations only need to touch one document, reserving transactions for the rare cases where you genuinely need cross-collection atomicity.
Was this helpful?
04
What is writeConcern and how does it affect durability?
writeConcern controls how many nodes acknowledge a write. w:1 acknowledges primary only; w:majority waits for replica set majority. Higher writeConcern increases durability but adds latency. Set to majority for critical data, 1 for logs or non-critical.