🍃 Expert MongoDB Interview Questions
This lesson targets senior DBAs, architects, and lead engineers. Topics include sharding internals, the oplog, read/write paths, query optimiser internals, Atlas Search, vector search, global clusters, performance anti-patterns, and production war stories. These questions separate MongoDB users from MongoDB architects.
Questions & Answers
01 What is Sharding in MongoDB? Explain the architecture. ►
Sharding Sharding is MongoDB’s horizontal scaling mechanism โ it distributes data across multiple servers (shards), each holding a subset of the total dataset. This enables storage and throughput that exceeds what any single server can handle.
Sharded cluster components:
- Shards โ each shard is a replica set holding a subset of the data. N shards = 1/N of data per shard.
- mongos โ the query router. Applications connect to mongos (not shards directly). It routes queries to the correct shard(s) and merges results.
- Config servers โ a replica set storing the cluster metadata: which shard holds which chunk of data (the chunk map).
// Enabling sharding on a database and collection
sh.enableSharding("myDatabase");
sh.shardCollection("myDatabase.orders", { customerId: "hashed" });
// or range-based: { createdAt: 1 }
Shard key choices:
- Hashed shard key โ even distribution across shards, but range queries scatter across all shards
- Range-based shard key โ efficient range queries, but risk of hotspots if inserts are monotonically increasing (e.g., timestamps)
- Zone sharding โ pin specific data ranges to specific shards (e.g., European user data stays on EU shards for GDPR compliance)
02 What is the MongoDB oplog and how does replication work internally? ►
Internals The oplog (operations log) is a special capped collection (local.oplog.rs) on the primary that records all write operations as idempotent transformation operations. Secondaries tail the oplog and replay operations to stay in sync.
Replication flow:
- Client sends a write to the primary
- Primary applies the write and appends an oplog entry
- Secondaries pull new oplog entries (they tail the oplog asynchronously)
- Each secondary applies the oplog entries to its own data in order
- Write concern
w:"majority"blocks the primary’s acknowledgement until a majority of secondaries confirm they’ve applied the write
// View the oplog
use local
db.oplog.rs.find().sort({ $natural: -1 }).limit(5)
// Each entry has: ts (timestamp), op ("i"=insert, "u"=update, "d"=delete), ns, o (operation)
Oplog size matters: The oplog is a capped collection with a fixed size. If a secondary falls behind faster than the oplog grows, it falls off the oplog and needs a full resync. Size the oplog large enough to cover the expected maximum secondary lag (Atlas manages this automatically).
03 What are the most damaging MongoDB anti-patterns and how do you fix them? ►
Performance
- Unbounded arrays โ storing an ever-growing array in a document. Documents have a 16MB limit; large arrays cause slow updates and bloated working sets. Fix: use the Bucket pattern, move to a separate collection, or cap with
$slice. - Massive number of collections โ thousands of collections (one per user, one per day). Each has index overhead. Fix: use a single collection with a discriminator field + index.
- Non-selective indexes โ indexing a boolean field (
isActive) with only two values is nearly useless. Fix: use a partial index or move the field to a compound index suffix. - Missing indexes on sort fields โ large in-memory sorts (
SORTstage in explain) block on a 32MB limit and cause slow queries. Fix: add an index that covers the sort. - Bloated working set โ fetching large documents when you only need 2 fields. Fix: always use projection; consider the Subset pattern.
- Write hotspots โ all writes going to one shard because the shard key is monotonically increasing (e.g.,
createdAt). Fix: use a hashed shard key or compound shard key with high-cardinality prefix. - One MongoClient per request โ creating a new connection instead of reusing the pool. Fix: create a singleton MongoClient at app startup.
04 How does the MongoDB query optimiser work? What is plan caching? ►
Internals When a query is first executed, the MongoDB query optimiser evaluates all candidate index plans in parallel (a process called trial runs) and selects the winning plan based on the lowest number of “works” (index lookups + document reads) to return the first batch of results.
Plan caching:
- The winning plan is cached for the specific query shape (filter structure, not values)
- Subsequent queries with the same shape use the cached plan without re-evaluating
- The cache is invalidated when: an index is added or dropped, the collection size crosses a threshold, or
planCacheClear()is called
// View cached plans for a collection
db.orders.getPlanCache().list()
// Force the use of a specific index (hint)
db.orders.find({ customerId: "c1", status: "pending" })
.hint({ customerId: 1, status: 1 });
// Clear the plan cache
db.orders.getPlanCache().clear();
Use .hint() to override the optimiser when you know a specific index is better. This is occasionally needed when the optimiser makes a poor choice for a specific data distribution.
05 What is Atlas Search and how does it differ from MongoDB’s text index? ►
Search Atlas Search is a full-text search engine built into MongoDB Atlas, powered by Apache Lucene. It is fundamentally more powerful than MongoDB’s native text index.
Atlas Search vs native text index:
- Multiple text indexes โ native allows only one; Atlas Search supports many search indexes with different configurations per collection
- Fuzzy matching โ Atlas Search supports typo tolerance (
fuzzy: { maxEdits: 1 }); native text index does not - Autocomplete โ Atlas Search has a dedicated
autocompleteoperator for as-you-type search; native text index doesn’t - Relevance scoring โ Atlas Search uses BM25 scoring with custom boost weights; native uses a simple term frequency score
- Faceted search โ Atlas Search supports
$searchMetafor facet counts; native text index does not - Highlighting โ returns matched text snippets with highlights; not available in native text index
db.articles.aggregate([
{ $search: {
index: "article_search",
compound: {
must: [{ text: { query: "mongodb performance", path: "body", fuzzy: { maxEdits: 1 } } }],
should: [{ text: { query: "mongodb performance", path: "title", score: { boost: { value: 3 } } } }]
}
}},
{ $limit: 10 },
{ $project: { title: 1, score: { $meta: "searchScore" } } }
]);
06 What is Vector Search in MongoDB Atlas? How does it work? ►
AI / ML MongoDB Atlas Vector Search stores and queries high-dimensional vector embeddings โ enabling semantic similarity search, recommendation systems, and AI-powered retrieval (RAG โ Retrieval Augmented Generation).
How it works:
- Convert text, images, or other data into numerical vector embeddings using a model (OpenAI, Cohere, HuggingFace)
- Store the embedding alongside the document in MongoDB
- Create a vector search index (uses HNSW algorithm for approximate nearest neighbour search)
- Query by finding the N nearest vectors to a query embedding
// Store a document with its embedding
await collection.insertOne({
title: "Introduction to MongoDB",
content: "MongoDB is a document database...",
embedding: [0.023, -0.441, 0.172, ...] // 1536-dim OpenAI embedding
});
// Create vector search index (via Atlas UI or API)
// { type: "vectorSearch", fields: [{ path: "embedding", dimensions: 1536,
// similarity: "cosine", type: "vector" }] }
// Query: find semantically similar documents
db.articles.aggregate([
{ $vectorSearch: {
index: "vector_index",
path: "embedding",
queryVector: queryEmbedding, // embedding of the user's search query
numCandidates: 150,
limit: 10
}},
{ $project: { title: 1, score: { $meta: "vectorSearchScore" } } }
]);
07 What is MongoDB’s Global Cluster feature in Atlas? ►
Atlas Global Clusters are MongoDB Atlas clusters that distribute data across multiple geographic regions with low-latency local reads and writes. They use Zone Sharding to pin data to specific regions.
Architecture:
- Data is sharded across regions using a location field as the shard key component
- Each region (zone) has its own set of shards
- Documents with a location field matching a zone’s range are stored in that zone’s shards
- European users’ data stays in the EU zone; Asian users’ data stays in the APAC zone
// Zone-aware shard key โ compound with location prefix
sh.shardCollection("mydb.users", { location: 1, userId: 1 });
// Assign a location range to a zone
sh.addTagRange(
"mydb.users",
{ location: "EU", userId: MinKey }, // all EU users
{ location: "EU", userId: MaxKey },
"EU_ZONE"
);
sh.addShardTag("shard-eu-1", "EU_ZONE");
Use cases: data residency compliance (GDPR, data sovereignty laws), low-latency global applications, multi-region disaster recovery with geographic failover.
08 What is the Two-Phase Commit pattern and when is it needed? ►
Transactions Before MongoDB 4.0 added multi-document transactions, the Two-Phase Commit (2PC) pattern was the standard way to simulate atomic multi-document operations. It is still relevant for understanding distributed systems and for systems that cannot use transactions.
The pattern uses a transaction document as a coordinator:
// Phase 1: Setup โ create a transaction record in "pending" state
db.transactions.insertOne({
_id: txnId, state: "pending",
from: "accountA", to: "accountB", amount: 100
});
// Apply changes with reference to the transaction
db.accounts.updateOne({ _id: "A", pendingTransactions: { $ne: txnId } },
{ $inc: { balance: -100 }, $push: { pendingTransactions: txnId } });
db.accounts.updateOne({ _id: "B", pendingTransactions: { $ne: txnId } },
{ $inc: { balance: 100 }, $push: { pendingTransactions: txnId } });
// Phase 2: Commit โ mark transaction applied
db.transactions.updateOne({ _id: txnId }, { $set: { state: "committed" } });
// Cleanup โ remove pending references
db.accounts.updateMany({ pendingTransactions: txnId },
{ $pull: { pendingTransactions: txnId } });
With MongoDB 4.0+ transactions, this entire pattern is replaced by a single session transaction. 2PC remains relevant for understanding eventual consistency models.
09 What is GridFS and when should you use it? ►
Storage GridFS is a specification for storing and retrieving files that exceed MongoDB’s 16MB document size limit. It splits files into chunks (default 255KB each) and stores them across two collections: fs.files (metadata) and fs.chunks (binary data).
// Using GridFS with the Node.js driver
const bucket = new GridFSBucket(db, { bucketName: "uploads" });
// Upload a file
const uploadStream = bucket.openUploadStream("report.pdf", {
metadata: { uploadedBy: "alice", contentType: "application/pdf" }
});
fs.createReadStream("./report.pdf").pipe(uploadStream);
// Download a file
const downloadStream = bucket.openDownloadStreamByName("report.pdf");
downloadStream.pipe(res); // pipe to HTTP response
// List files
const files = await bucket.find({ "metadata.uploadedBy": "alice" }).toArray();
// Delete a file
await bucket.delete(fileId);
When to use GridFS vs alternatives:
- Use GridFS when: files are >16MB, you need to query file metadata, you want to stream large files, or your infrastructure doesn’t include dedicated object storage
- Use S3/GCS/Azure Blob (preferred for production) when: you need CDN integration, lower cost at scale, better performance for large files, presigned URLs for direct browser uploads
10 How do you perform a zero-downtime MongoDB schema migration? ►
Operations MongoDB’s flexible schema means you can add fields, but removing or renaming fields while the application is running requires a careful migration strategy to avoid downtime or errors.
The Expand-Contract (parallel change) pattern:
- Step 1 โ Expand: Deploy new code that writes to BOTH the old field name and the new field name. Reads from the old field. Old code continues to work.
- Step 2 โ Migrate: Run a background migration script to backfill the new field on existing documents:
db.users.updateMany({ newField: { $exists: false } }, [{ $set: { newField: "$oldField" } }]) - Step 3 โ Switch: Deploy code that reads from the new field only. Still writes to both for a brief overlap period.
- Step 4 โ Contract: After all instances are on the new code, stop writing to the old field. Run a cleanup to
$unsetthe old field.
// Step 2: Backfill migration (run in batches to avoid locks)
const batchSize = 1000;
let processed = 0;
while (true) {
const result = await db.collection("users").updateMany(
{ newField: { $exists: false } },
[{ $set: { newField: "$oldField" } }],
{ hint: { _id: 1 } }
);
processed += result.modifiedCount;
if (result.modifiedCount === 0) break;
await new Promise(r => setTimeout(r, 100)); // brief pause
}
11 What is the difference between $addToSet, $push with $addToSet, and set operations in aggregation? ►
Operations
// $addToSet (update operator) โ appends only if value doesn't exist
db.users.updateOne({ _id: id }, { $addToSet: { tags: "mongodb" } });
// If "mongodb" is already in tags โ no change. If not โ appended.
// $push with $addToSet modifier โ same in update context, but $addToSet is preferred
// $push with $each โ add multiple values at once
db.users.updateOne({ _id: id }, {
$push: { tags: { $each: ["nosql", "database"], $slice: -10 } } // keep last 10
});
// Aggregation set operators (for computing set operations in pipelines)
db.users.aggregate([
{ $project: {
union: { $setUnion: ["$skillsA", "$skillsB"] },
intersection: { $setIntersection: ["$skillsA", "$skillsB"] },
difference: { $setDifference: ["$skillsA", "$skillsB"] }, // in A not B
isSubset: { $setIsSubset: ["$skillsA", "$skillsB"] }
}}
]);
// $addToSet as aggregation accumulator โ collect unique values in $group
db.orders.aggregate([
{ $group: {
_id: "$customerId",
uniqueProducts: { $addToSet: "$productId" } // set of unique products bought
}}
]);
12 What is MongoDB’s Queryable Encryption? ►
Security Queryable Encryption (QE), introduced in MongoDB 6.0 (GA in 7.0), allows applications to encrypt sensitive fields client-side while still being able to run equality and range queries on the encrypted data โ without the server ever seeing the plaintext values.
How it works:
- Sensitive fields (SSN, credit card number, salary) are encrypted by the MongoDB driver before being sent to the server
- MongoDB stores the encrypted data and special indexed metadata
- When querying, the driver encrypts the query value and sends an encrypted predicate; MongoDB matches without decrypting
- Only the application (with the encryption key) can decrypt the results
// Client-side field level encryption setup
const encryptedFields = {
fields: [
{ path: "ssn", bsonType: "string", queries: { queryType: "equality" } },
{ path: "salary", bsonType: "int", queries: { queryType: "range" } },
{ path: "creditCard", bsonType: "string" } // encrypted, not queryable
]
};
QE provides protection against privileged insiders, compromised database administrators, and data breaches โ even if the database server is compromised, the plaintext data is never exposed. This is essential for HIPAA, PCI-DSS, and GDPR compliance scenarios.
13 How do you design a real-time leaderboard in MongoDB at scale? ►
Design A leaderboard has two main operations: update a player’s score (high write throughput) and read the top N players sorted by score (low-latency read).
// Schema
{
_id: "player_alice",
displayName: "Alice",
score: 98750,
rank: null, // optionally pre-computed
lastUpdated: ISODate("...")
}
// Index for leaderboard reads (score descending)
db.leaderboard.createIndex({ score: -1 });
// Update score atomically โ increment by earned points
db.leaderboard.updateOne(
{ _id: "player_alice" },
{ $inc: { score: earnedPoints }, $set: { lastUpdated: new Date() } },
{ upsert: true }
);
// Read top 100
db.leaderboard.find({}, { displayName: 1, score: 1 })
.sort({ score: -1 }).limit(100);
// Get a player's rank (expensive without pre-computation)
const rank = await db.leaderboard.countDocuments({ score: { $gt: myScore } }) + 1;
Scaling strategies: Use Redis for the hot path (sorted sets for O(log N) rank updates) and sync to MongoDB periodically. Use Change Streams to update a materialised “top-100” cache document on every score change. For global leaderboards, use Zone Sharding by region.
14 What is the MongoDB Aggregation $graphLookup stage? ►
Aggregation $graphLookup performs a recursive graph traversal within a collection โ finding all ancestors, descendants, or connected nodes in a hierarchical or graph structure.
// Organisational hierarchy โ find all reports under a manager (any depth)
db.employees.aggregate([
{ $match: { name: "CEO" } },
{ $graphLookup: {
from: "employees",
startWith: "$directReports", // starting point
connectFromField: "directReports", // field to follow recursively
connectToField: "name", // field to match against
as: "allReports", // output field
maxDepth: 5, // optional: limit recursion depth
depthField: "depth" // add depth level to each result
}},
{ $project: { name: 1, "allReports.name": 1, "allReports.depth": 1 } }
]);
// Other use cases:
// - Find all connected friends (social graph)
// - Trace all ancestor categories in a product taxonomy
// - Find all transitive dependencies in a dependency graph
// - Route finding in a road or network graph
Note: $graphLookup loads all recursively connected documents into memory. Use maxDepth to prevent runaway queries on deep or circular graphs. Requires an index on connectToField for performance.
15 How does MongoDB handle the CAP theorem? ►
Theory The CAP theorem states that a distributed system can guarantee at most two of: Consistency, Availability, and Partition Tolerance. Since network partitions always occur in practice, real systems choose between CP (consistency + partition tolerance) and AP (availability + partition tolerance).
MongoDB’s position: MongoDB is primarily a CP system โ it prioritises consistency over availability during a network partition.
- During a primary failure, the replica set holds an election. Until a new primary is elected (typically 10-30 seconds), the cluster refuses writes โ sacrificing availability for consistency.
- With
writeConcern: { w: "majority" }, writes are only acknowledged after a majority confirms them โ ensuring data is not lost even if the primary fails. - With
readPreference: "secondary", you can trade consistency for availability (reads from secondaries that may be slightly behind).
MongoDB lets you tune the CP/AP trade-off via write concern and read preference. Stricter settings (w: majority, readConcern: majority) move further toward CP. Relaxed settings (w:0, reading from secondaries) move toward AP.
16 What is the Bucket Pattern for time-series data in MongoDB? ►
Patterns The Bucket Pattern groups time-series readings into document “buckets” covering a fixed time window (e.g., one hour, one day). Instead of one document per reading, you have one document per bucket containing an array of readings.
// Without bucketing โ one document per reading (bad for IoT scale)
{ sensorId: "s1", timestamp: ISODate("..."), temperature: 22.5 }
{ sensorId: "s1", timestamp: ISODate("..."), temperature: 22.7 }
// Millions of tiny documents = slow queries, high index overhead
// With bucketing โ one document per sensor per hour
{
sensorId: "sensor-001",
bucketStart: ISODate("2026-04-16T09:00:00Z"),
bucketEnd: ISODate("2026-04-16T09:59:59Z"),
count: 60,
sumTemp: 1354.2, // pre-computed for fast averages
minTemp: 22.1,
maxTemp: 23.0,
readings: [
{ t: ISODate("2026-04-16T09:00:00Z"), v: 22.5 },
{ t: ISODate("2026-04-16T09:01:00Z"), v: 22.7 },
// ... up to 60 readings per bucket
]
}
// Add new reading โ append to bucket
db.sensorData.updateOne(
{ sensorId: "sensor-001", count: { $lt: 60 }, bucketStart: currentHour },
{ $push: { readings: newReading }, $inc: { count: 1 }, $min: { minTemp: newReading.v }, $max: { maxTemp: newReading.v } },
{ upsert: true }
);
MongoDB’s native Time Series collections (5.0+) implement the Bucket Pattern automatically โ use them for new projects instead of manual bucketing.
17 What is MongoDB’s Aggregation $setWindowFields stage? ►
Aggregation $setWindowFields (MongoDB 5.0+) enables window functions โ computing values over a sliding or fixed window of documents relative to the current document. Similar to SQL window functions (OVER, PARTITION BY, ROW_NUMBER).
db.sales.aggregate([
{ $setWindowFields: {
partitionBy: "$salesRep", // group by (like SQL PARTITION BY)
sortBy: { orderDate: 1 }, // order within partition
output: {
// Running total of sales per rep (cumulative sum)
runningTotal: {
$sum: "$amount",
window: { documents: ["unbounded", "current"] }
},
// 7-day moving average
movingAvg7d: {
$avg: "$amount",
window: { range: [-6, 0], unit: "day" }
},
// Rank within partition
rank: { $rank: {} },
// Previous value (LAG)
prevAmount: {
$shift: { output: "$amount", by: -1, default: 0 }
}
}
}}
]);
This replaces complex $group + $lookup self-join workarounds previously required for running totals, moving averages, and rankings.
18 How do you tune MongoDB for a write-heavy workload? ►
Performance
- Reduce index count โ every index adds overhead to every write. Audit indexes with
db.collection.aggregate([{ $indexStats: {} }])and drop unused ones. - Loosen write concern for non-critical writes โ use
w:1(primary ack only) orw:0(fire and forget) for logging, analytics, or telemetry writes where occasional loss is acceptable. - Use bulk writes โ
bulkWrite()withordered: falsebatches multiple operations in a single round trip and maximises parallelism. - Avoid document growth โ documents that grow beyond their allocated space are moved on disk (called a document relocation/migration). Use
$seton fixed-size fields or pre-allocate space. - Use hashed shard keys โ distributes writes evenly across shards. Monotonically increasing keys (timestamps, auto-increment IDs) cause write hotspots on the last shard.
- Increase WiredTiger cache โ a larger cache absorbs more writes in memory before flushing to disk. Target 60-70% cache hit ratio.
- Use SSDs โ WiredTiger is I/O intensive during checkpoints. NVMe SSDs dramatically outperform HDDs for write-heavy workloads.
19 What is MongoDB’s Aggregation $densify stage? ►
Aggregation $densify (MongoDB 5.1+) fills in missing values in a sequence โ adding documents for values that don’t exist in the dataset. Useful for time-series visualisation where gaps in data should show as zero rather than being absent.
// Sales data has no records on days with no sales
// Without $densify โ a chart would skip those days entirely
// With $densify โ we get a document for every day in the range
db.dailySales.aggregate([
{ $densify: {
field: "date",
range: {
step: 1,
unit: "day",
bounds: [ ISODate("2026-01-01"), ISODate("2026-01-31") ]
},
partitionByFields: ["region"] // fill gaps per region separately
}},
// Now set revenue to 0 for filled-in dates
{ $addFields: { revenue: { $ifNull: ["$revenue", 0] } } }
]);
// Result: a document for every day Jan 1-31 per region
// Days with no sales have revenue: 0 instead of being missing
Pairs naturally with $setWindowFields for time-series analytics โ densify first to fill gaps, then compute moving averages without skewing them with missing data points.
20 How would you architect a multi-tenant SaaS application with MongoDB? ►
Architecture Multi-tenancy in MongoDB has three main approaches, each with different isolation, cost, and complexity trade-offs:
1. Database-per-tenant (highest isolation):
// Each tenant has their own database db_tenant_acme.orders / db_tenant_globex.orders // Strong isolation, easy to delete/export a tenant, but high resource overhead // Best for: regulated industries, large enterprise tenants, <100 tenants
2. Collection-per-tenant (medium isolation):
// Each tenant has their own collection in a shared database acme_orders / globex_orders // Moderate isolation, flexible indexing per tenant // Best for: medium-scale SaaS, 100-10,000 tenants
3. Shared collection with tenantId (lowest isolation, most scalable):
// All tenants share collections, discriminated by tenantId field
{ _id: ..., tenantId: "acme", orderId: "O-001", ... }
// Index on tenantId + query fields: { tenantId: 1, status: 1, createdAt: -1 }
// Every query MUST include tenantId in the filter
// Best for: SaaS with thousands of tenants, standardised schemas, cost efficiency
// Enforce tenantId in every query via middleware layer
const tenantDb = {
find: (filter) => db.collection("orders").find({ ...filter, tenantId: ctx.tenantId })
};
For large SaaS: use the shared collection approach with a strict repository pattern that injects tenantId automatically. Use partial indexes to keep per-tenant index sizes manageable.
21 What is MongoDB’s Aggregation $search with the autocomplete operator? ►
Search Atlas Search’s autocomplete operator provides as-you-type search suggestions by matching partial word prefixes. It is far more efficient than using regex queries for search-as-you-type.
// 1. Create an Atlas Search index with autocomplete mapping
// { mappings: { fields: { name: [
// { type: "autocomplete", analyzer: "lucene.standard", tokenization: "edgeGram", minGrams: 2, maxGrams: 15 }
// ] } } }
// 2. Query with autocomplete โ partial "mong" matches "MongoDB", "Mongoose", etc.
db.products.aggregate([
{ $search: {
index: "product_search",
autocomplete: {
query: "mong",
path: "name",
fuzzy: { maxEdits: 1 } // tolerate one typo
}
}},
{ $limit: 10 },
{ $project: { name: 1, category: 1, score: { $meta: "searchScore" } } }
]);
// 3. Highlight matched tokens
db.products.aggregate([
{ $search: {
autocomplete: { query: "node", path: "name" },
highlight: { path: "name" }
}},
{ $project: { name: 1, highlights: { $meta: "searchHighlights" } } }
]);
The edgeGram tokenisation splits “MongoDB” into tokens: “mo”, “mon”, “mong”, “mongo”, etc. โ enabling prefix matching at any position. Use rightEdgeGram for suffix matching (searching by ending characters).
📝 Knowledge Check
These questions mirror real senior-level MongoDB architecture and internals interview scenarios.