MongoDB interviews should go beyond a simplistic "SQL vs NoSQL" debate. The useful question is whether you can connect the document model to access patterns, consistency, indexing, and operational trade-offs.
This guide covers 22 MongoDB interview questions, from fundamentals to aggregation pipelines, schema design, and scaling. The 2026 revision was checked against the MongoDB 8.3 documentation.
Table of Contents
- MongoDB Fundamentals Questions
- Mongoose Schema Questions
- Schema Design Questions
- CRUD Operations Questions
- Aggregation Pipeline Questions
- Indexing Questions
- Connection and Transaction Questions
- Scaling and Performance Questions
MongoDB Fundamentals Questions
Understanding MongoDB's core concepts is essential for any interview involving NoSQL databases.
What are documents and collections in MongoDB?
MongoDB stores data as BSON documents, which are JSON-like structures with additional data types. A collection is analogous to a table in relational databases, containing multiple documents. Unlike SQL rows, documents in the same collection can have different fields and structures.
The key differences from SQL include schema flexibility (documents can have varying fields), first-class support for nested objects and arrays, and a design philosophy that often favors denormalization over JOINs.
// MongoDB stores data as BSON documents (JSON-like)
// Collection = table, Document = row
// A document in the "users" collection
{
_id: ObjectId("507f1f77bcf86cd799439011"),
name: "Sarah Chen",
email: "sarah@example.com",
profile: {
bio: "Full-stack developer",
skills: ["Node.js", "MongoDB", "React"]
},
createdAt: ISODate("2024-01-15T10:30:00Z")
}When should you use MongoDB instead of a relational database?
The decision starts with data shape and access patterns, then includes consistency, constraints, scaling, operations, cost, and team expertise. MongoDB is a strong fit when bounded aggregates are read and changed as documents, when nested data is natural, or when its sharding and platform features match the workload.
Relational databases are often a strong fit for relationship-heavy models, ad hoc joins, and declarative cross-row constraints. This is not a consistency divide: MongoDB supports schema validation and multi-document transactions, while relational systems can store JSON and scale horizontally. Polyglot persistence is justified only when the benefits outweigh running and integrating another database.
How does MongoDB compare to SQL databases?
MongoDB and SQL databases have fundamentally different data models and terminology. Understanding these mappings helps when transitioning between the two or explaining concepts in interviews.
| SQL | MongoDB | Mongoose |
|---|---|---|
| Table | Collection | Model |
| Row | Document | Document instance |
| Column | Field | Schema field |
| Primary Key | _id (ObjectId) | _id |
| Foreign Key | Reference (ObjectId) | ref + populate() |
| JOIN | $lookup / populate | .populate() |
| GROUP BY | $group | .aggregate() |
| INDEX | createIndex() | schema.index() |
| Transaction | session.withTransaction() | session.withTransaction() |
Mongoose Schema Questions
Mongoose provides structure and validation on top of MongoDB's flexible document model.
What is Mongoose and why would you use it?
Mongoose is an ODM (Object Document Mapper) for MongoDB and Node.js that provides application-level schemas, validation, type casting, query building, middleware hooks, and model behavior. MongoDB has a flexible schema by default and can also enforce collection validation with $jsonSchema and query expressions.
Mongoose simplifies common operations through a fluent query API, adds virtual properties and instance methods to documents, and provides middleware for pre/post hooks on operations like save and validate.
How do you define a Mongoose schema with validation?
A Mongoose schema defines the structure, validation rules, and behavior for documents in a collection. You specify field types, required fields, default values, custom validators, and indexes. The schema also supports virtual properties, instance methods, static methods, and middleware hooks.
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Name is required'],
trim: true,
maxlength: 100
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^\S+@\S+\.\S+$/, 'Invalid email format']
},
password: {
type: String,
required: true,
minlength: 8,
select: false // Don't include in queries by default
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
profile: {
bio: { type: String, maxlength: 500 },
avatar: String,
skills: [String]
},
loginAttempts: { type: Number, default: 0 },
lockUntil: Date
}, {
timestamps: true, // Adds createdAt and updatedAt
toJSON: { virtuals: true }
});
// Indexes for query performance (`unique: true` already declares the email index)
userSchema.index({ 'profile.skills': 1 });
userSchema.index({ createdAt: -1 });
// Virtual property (not stored in DB)
userSchema.virtual('isLocked').get(function() {
return this.lockUntil && this.lockUntil > Date.now();
});
// Instance method
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
// Static method
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
// Pre-save middleware
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
this.password = await bcrypt.hash(this.password, 12);
next();
});
const User = mongoose.model('User', userSchema);How do you ensure data quality without a database schema?
Mongoose can validate and cast application writes, but it is only one client boundary. Use MongoDB collection validation when rules must apply to every writer, and use unique indexes for uniqueness. Validation levels and actions control how new rules affect existing or invalid documents; migrations and monitoring still matter.
Schema Design Questions
Schema design is the most important architectural decision in MongoDB applications.
When should you embed data in MongoDB?
Embedding (denormalization) stores related data in a single document. This approach works best when the data is always accessed together with the parent document, when you have a one-to-few relationship, and when the embedded data rarely changes independently from the parent.
Common examples include embedding addresses in a user document or embedding line items in an order. The benefit is that a single query returns all the data you need without additional lookups.
// GOOD: Embed addresses in user document
// - Accessed together with user
// - One-to-few relationship
// - Rarely updated independently
const userSchema = new mongoose.Schema({
name: String,
addresses: [{
street: String,
city: String,
zipCode: String,
isDefault: Boolean
}]
});
// Query returns everything in one call
const user = await User.findById(userId);
console.log(user.addresses[0].city);When should you reference data instead of embedding?
Referencing (normalization) stores relationships as ObjectId references to documents in other collections. This approach is better when the data is accessed independently from the parent, when you have one-to-many or many-to-many relationships, when the related data could grow unboundedly, or when documents would exceed MongoDB's 16MB size limit.
Use Mongoose's populate() method or the $lookup aggregation stage to join referenced data when needed.
// GOOD: Reference orders separately
// - Accessed independently
// - One-to-many relationship (user has many orders)
// - Orders grow unboundedly
const orderSchema = new mongoose.Schema({
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
index: true
},
items: [{
product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
quantity: Number,
price: Number
}],
total: Number,
status: String
});
// Populate to join data
const orders = await Order.find({ user: userId })
.populate('user', 'name email')
.populate('items.product', 'name price');What factors determine embedding vs referencing?
The decision between embedding and referencing depends on several factors. Consider the relationship cardinality, how data is accessed, update frequency, document size limits, and whether data duplication is acceptable.
| Factor | Embed | Reference |
|---|---|---|
| Relationship | One-to-few | One-to-many, Many-to-many |
| Access pattern | Usually read together | Often separate |
| Update frequency | Rarely changes | Changes independently |
| Document size | Safely bounded below 16 MiB | Could grow large |
| Data duplication | Acceptable | Problematic |
A practical example: embed a bounded set of addresses when the application usually fetches them with the user profile. Reference orders because their count can grow without bound, they have an independent lifecycle, and they are queried separately.
How would you design a schema for a blog platform?
A blog platform can combine patterns. Users and comments are separate collections, while a post may keep a small denormalized preview of recent comments. Treat that preview as a rebuildable cache: otherwise the same comment has two sources of truth that can diverge.
// Users - standalone collection
const userSchema = new Schema({
username: { type: String, unique: true },
email: { type: String, unique: true },
passwordHash: String,
profile: {
displayName: String,
bio: String,
avatar: String
}
});
// Posts - references author, embeds limited comments
const postSchema = new Schema({
author: { type: ObjectId, ref: 'User', index: true },
title: String,
slug: { type: String, unique: true },
content: String,
tags: { type: [String], index: true },
status: { type: String, enum: ['draft', 'published'] },
// Embed recent comments (limit to prevent unbounded growth)
recentComments: [{
author: { type: ObjectId, ref: 'User' },
content: String,
createdAt: Date
}],
commentCount: { type: Number, default: 0 },
viewCount: { type: Number, default: 0 }
}, { timestamps: true });
// Full comments - separate collection for scalability
const commentSchema = new Schema({
post: { type: ObjectId, ref: 'Post', index: true },
author: { type: ObjectId, ref: 'User' },
content: String,
parentComment: { type: ObjectId, ref: 'Comment' } // For threading
}, { timestamps: true });
// Indexes for common queries
postSchema.index({ author: 1, createdAt: -1 });
postSchema.index({ tags: 1, status: 1 });If the application updates the comment and its embedded preview synchronously, use a transaction. An event-driven projection can instead update the preview asynchronously, provided the UI tolerates lag and the projection is idempotent.
CRUD Operations Questions
Understanding MongoDB's CRUD operations is fundamental for any Node.js developer working with the database.
How do you create documents in MongoDB?
MongoDB provides several methods for creating documents. The create() method inserts a single document with validation, while insertMany() efficiently inserts multiple documents in a single operation. Always handle validation errors and duplicate key violations appropriately.
// Single document
const user = await User.create({
name: 'John Doe',
email: 'john@example.com',
password: 'securePassword123'
});
// Multiple documents
const users = await User.insertMany([
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
]);
// With validation handling
try {
const user = await User.create(userData);
} catch (error) {
if (error.code === 11000) {
// Duplicate key error (unique constraint)
throw new Error('Email already exists');
}
if (error.name === 'ValidationError') {
// Mongoose validation failed
const messages = Object.values(error.errors).map(e => e.message);
throw new Error(messages.join(', '));
}
throw error;
}How do you query documents in MongoDB?
MongoDB offers flexible querying with methods like findById(), findOne(), and find(). Query builders chain projection, sorting, and pagination. Mongoose's lean() returns plain JavaScript objects without document hydration, virtuals, getters, change tracking, or save(), reducing overhead when those features are not needed.
// Find one
const user = await User.findById(id);
const user = await User.findOne({ email: 'john@example.com' });
// Find many with query builders
const users = await User.find({ role: 'admin' })
.select('name email createdAt') // Only these fields
.sort({ createdAt: -1 }) // Newest first
.skip(20) // Pagination offset
.limit(10) // Page size
.lean(); // Plain objects with lower hydration overhead
// Complex queries
const users = await User.find({
createdAt: { $gte: new Date('2024-01-01') },
'profile.skills': { $in: ['Node.js', 'MongoDB'] },
role: { $ne: 'admin' }
});
// Text search (requires text index)
const results = await Product.find(
{ $text: { $search: 'wireless bluetooth' } },
{ score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });How do you update documents in MongoDB?
MongoDB provides update methods with different return values. updateOne() returns an update result; Mongoose's findByIdAndUpdate() returns the document and needs { new: true } (or returnDocument: 'after' in driver-style APIs) to return the post-update version. Operators such as $set, $inc, $push, and $pull modify selected fields.
// Update one document
const result = await User.updateOne(
{ _id: userId },
{ $set: { 'profile.bio': 'Updated bio' } }
);
// Find and update (returns the document)
const user = await User.findByIdAndUpdate(
userId,
{ $inc: { loginAttempts: 1 } },
{ new: true, runValidators: true } // Return updated doc, run validators
);
// Update operators
await User.updateOne({ _id: userId }, {
$set: { name: 'New Name' }, // Set field value
$unset: { tempField: '' }, // Remove field
$inc: { loginCount: 1 }, // Increment number
$push: { 'profile.skills': 'GraphQL' }, // Add to array
$pull: { 'profile.skills': 'jQuery' }, // Remove from array
$addToSet: { tags: 'verified' } // Add to array if not exists
});
// Bulk updates
await User.updateMany(
{ lastLogin: { $lt: new Date('2023-01-01') } },
{ $set: { status: 'inactive' } }
);How do you delete documents in MongoDB?
MongoDB supports hard-delete operations, while applications can model soft deletion with a deletedAt timestamp. Query middleware can filter many soft-deleted reads, but it is not a database-wide access-control rule.
// Delete one
await User.deleteOne({ _id: userId });
const user = await User.findByIdAndDelete(userId);
// Delete many
const result = await User.deleteMany({ status: 'inactive' });
console.log(`Deleted ${result.deletedCount} users`);
// Soft-delete sketch; production code must cover every read path
const userSchema = new mongoose.Schema({
// ... other fields
deletedAt: Date
});
userSchema.pre(/^find/, function() {
this.where({ deletedAt: null });
});
userSchema.methods.softDelete = function() {
this.deletedAt = new Date();
return this.save();
};Soft deletion is a product and compliance decision, not a universal default. Query middleware like this does not automatically protect every aggregation, populate, direct-driver call, unique-index rule, or administrative query; test all access paths and define retention and purge behavior.
Aggregation Pipeline Questions
The aggregation pipeline is MongoDB's most powerful feature for data analysis and transformation.
What is the MongoDB aggregation pipeline and how does it work?
The aggregation pipeline passes documents through ordered processing stages, with each stage's output becoming the next stage's input. It covers filtering, grouping, reshaping, joins, window calculations, search integrations, and other transformations; compare it with relational query plans by capability and workload rather than claiming one model is universally more flexible.
// Sales report: total revenue by product category
const report = await Order.aggregate([
// Stage 1: Filter to completed orders this year
{
$match: {
status: 'completed',
createdAt: { $gte: new Date('2024-01-01') }
}
},
// Stage 2: Unwind the items array (one doc per item)
{ $unwind: '$items' },
// Stage 3: Lookup product details
{
$lookup: {
from: 'products',
localField: 'items.product',
foreignField: '_id',
as: 'productInfo'
}
},
// Stage 4: Flatten the lookup result
{ $unwind: '$productInfo' },
// Stage 5: Group by category
{
$group: {
_id: '$productInfo.category',
totalRevenue: { $sum: { $multiply: ['$items.quantity', '$items.price'] } },
totalOrders: { $sum: 1 },
avgOrderValue: { $avg: { $multiply: ['$items.quantity', '$items.price'] } }
}
},
// Stage 6: Sort by revenue descending
{ $sort: { totalRevenue: -1 } },
// Stage 7: Reshape output
{
$project: {
category: '$_id',
totalRevenue: { $round: ['$totalRevenue', 2] },
totalOrders: 1,
avgOrderValue: { $round: ['$avgOrderValue', 2] },
_id: 0
}
}
]);What are the most commonly used aggregation stages?
The aggregation pipeline has many stages, but certain ones appear in almost every pipeline. Understanding these core stages helps you build complex data transformations.
// $match - Filter documents (like WHERE)
{ $match: { status: 'active', age: { $gte: 18 } } }
// $group - Aggregate values (like GROUP BY)
{ $group: {
_id: '$category',
count: { $sum: 1 },
avgPrice: { $avg: '$price' },
maxPrice: { $max: '$price' },
items: { $push: '$name' } // Collect into array
}}
// $project - Reshape documents (like SELECT)
{ $project: {
name: 1,
email: 1,
fullName: { $concat: ['$firstName', ' ', '$lastName'] },
year: { $year: '$createdAt' }
}}
// $lookup - Join collections (like LEFT JOIN)
{ $lookup: {
from: 'orders',
localField: '_id',
foreignField: 'userId',
as: 'userOrders'
}}
// $unwind - Flatten arrays
{ $unwind: '$tags' } // One document per tag
// $sort, $skip, $limit - Pagination
{ $sort: { createdAt: -1 } },
{ $skip: 20 },
{ $limit: 10 }
// $facet - Multiple pipelines in parallel
{ $facet: {
results: [{ $skip: 0 }, { $limit: 10 }],
totalCount: [{ $count: 'count' }]
}}Indexing Questions
Proper indexing is critical for MongoDB performance at scale.
How do you create indexes in MongoDB?
Indexes can reduce work for matching, sorting, and covered projections. Define them in migrations or controlled Mongoose synchronization and verify them against production-shaped queries. MongoDB supports single-field, compound, text, geospatial, wildcard, hashed, and TTL indexes. TTL deletion is performed by a background process and is not guaranteed at the exact expiry instant.
// In Mongoose schema
const productSchema = new mongoose.Schema({
name: { type: String, index: true }, // Single field
sku: { type: String, unique: true }, // Unique index
category: String,
price: Number,
tags: [String],
description: String
});
// Compound index (queries using both fields)
productSchema.index({ category: 1, price: -1 });
// Text index for search
productSchema.index({ name: 'text', description: 'text' });
// TTL index (auto-delete after time)
const sessionSchema = new mongoose.Schema({
userId: ObjectId,
expiresAt: { type: Date, index: { expires: 0 } } // Delete when expiresAt passes
});
// Programmatically
await Product.collection.createIndex({ category: 1, price: -1 });What is a good indexing strategy for MongoDB?
A good indexing strategy starts with query patterns. Use explain('executionStats') to compare returned documents with keys and documents examined, observe sorts, and check winning and rejected plans. For compound indexes, order matters: {a, b, c} supports its prefixes and can sometimes help queries containing a prefix plus later fields, but it does not efficiently support a query on only {b} or {c}.
Follow the ESR rule for compound index field ordering: Equality fields first, then Sort fields, then Range fields.
// Check if query uses index
const explanation = await User.find({ email: 'test@example.com' })
.explain('executionStats');
console.log(explanation.executionStats.executionStages.stage);
// Evaluate keys/docs examined and sort work; the stage name alone is insufficient.
// Index intersection vs compound index
// If you query: { category: 'electronics', brand: 'Apple' }
// Option 1: Two single indexes (MongoDB may intersect)
productSchema.index({ category: 1 });
productSchema.index({ brand: 1 });
// Option 2: Compound index (more efficient for this specific query)
productSchema.index({ category: 1, brand: 1 });
// Compound index order matters!
// Index { a: 1, b: 1, c: 1 } supports:
// - Queries on { a }
// - Queries on { a, b }
// - Queries on { a, b, c }
// But NOT queries on just { b } or { c }Connection and Transaction Questions
Understanding connection management and transactions is essential for production MongoDB applications.
How do you manage MongoDB connections in Node.js?
Proper connection management includes setting pool sizes, handling connection events, and implementing graceful shutdown. Mongoose maintains a connection pool that reuses connections for efficiency. Configure the pool size based on your application's concurrency needs.
// Connection with best practices
const mongoose = require('mongoose');
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGODB_URI, {
maxPoolSize: 10, // Connection pool size
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
});
console.log('MongoDB connected');
} catch (error) {
console.error('MongoDB connection error:', error);
process.exit(1);
}
};
// Handle connection events
mongoose.connection.on('error', err => {
console.error('MongoDB error:', err);
});
mongoose.connection.on('disconnected', () => {
console.warn('MongoDB disconnected. Attempting reconnect...');
});
// Graceful shutdown
process.on('SIGINT', async () => {
await mongoose.connection.close();
console.log('MongoDB connection closed due to app termination');
process.exit(0);
});How do you handle transactions in MongoDB?
MongoDB supports multi-document ACID transactions on replica sets and sharded clusters. Use the driver's withTransaction() or Mongoose's Connection#transaction() helper so transient transaction and commit errors can be retried according to the driver rules. A retried callback may run more than once, so avoid non-idempotent external side effects inside it.
Each individual write operation is atomic at the document level. Embedding related state can therefore remove some multi-document transactions, but use a transaction when one invariant genuinely spans documents. Pass the session to every operation and do not run parallel operations such as Promise.all() inside a transaction.
// Mongoose transaction helper
await mongoose.connection.transaction(async (session) => {
await Account.findByIdAndUpdate(
fromAccountId,
{ $inc: { balance: -100 } },
{ session }
);
await Account.findByIdAndUpdate(
toAccountId,
{ $inc: { balance: 100 } },
{ session }
);
});Scaling and Performance Questions
Understanding how to scale MongoDB and optimize performance is crucial for senior-level interviews.
How do you handle the N+1 query problem in MongoDB?
The N+1 problem occurs when code fetches a list and then issues a related query per item. Bounded embedding can remove extra queries; $lookup can join on the server; DataLoader can batch and cache within a request. Mongoose populate() may batch IDs into additional queries, but query count depends on paths, models, and options such as per-document limits—measure instead of assuming one query.
How do you scale MongoDB?
Replica sets provide redundancy and automatic primary election. Secondary reads can distribute eligible read traffic, but they may be stale depending on read preference and read concern and do not increase primary write capacity. Sharding partitions data across shards for workloads that outgrow one replica set; shard-key choice determines routing, distribution, and hotspot risk.
There is no meaningful universal document-count threshold: document size, working set, indexes, query shape, throughput, latency, and hardware matter. Shard only for measured capacity, throughput, or data-locality needs, and use zone sharding deliberately when geography or residency must influence placement.
Quick Reference
| SQL | MongoDB | Mongoose |
|---|---|---|
| Table | Collection | Model |
| Row | Document | Document instance |
| Column | Field | Schema field |
| Primary Key | _id (ObjectId) | _id |
| Foreign Key | Reference (ObjectId) | ref + populate() |
| JOIN | $lookup / populate | .populate() |
| GROUP BY | $group | .aggregate() |
| INDEX | createIndex() | schema.index() |
| Transaction | session.withTransaction() | session.withTransaction() |
Related Articles
If you found this helpful, check out these related guides:
- Complete Node.js Backend Developer Interview Guide - comprehensive preparation guide for backend interviews
- PostgreSQL & Node.js Interview Guide - When to choose SQL over MongoDB
- SQL JOINs Interview Guide - Master relational database joins
- Node.js Advanced Interview Guide - Event loop, streams, and Node.js internals
- REST API Interview Guide - API design principles and best practices
Official References
- MongoDB 8.3 release notes
- MongoDB data modeling
- MongoDB schema validation
- MongoDB transactions
- MongoDB compound indexes and prefixes
- MongoDB TTL indexes
- Mongoose transactions
Frequently Asked Questions
When should you use MongoDB instead of a relational database?
Choose from access patterns, consistency requirements, constraints, operations, and team expertise—not from a SQL-versus-NoSQL slogan. MongoDB fits aggregates that are read and changed as documents and benefits from a flexible schema and built-in sharding. A relational database often fits relationship-heavy workloads and declarative constraints. Both support indexes, transactions, validation, and horizontal-scaling options with different trade-offs.
What is the difference between embedding and referencing in MongoDB?
Embedding stores related data in one document; referencing stores identifiers that are resolved with another query, populate, or $lookup. Embed bounded data that is read and updated with its parent. Reference data with an independent lifecycle, unbounded growth, many-to-many reuse, or a risk of approaching MongoDB's 16 MiB BSON document limit.
What is the MongoDB aggregation pipeline?
The aggregation pipeline passes documents through ordered stages. Common stages include $match, $group, $sort, $project, $lookup, and $unwind. Each stage consumes the previous stage's output. Put selective $match stages early when possible, inspect the execution plan, and remember that some stages can use indexes only in particular positions and forms.
How do you create indexes in MongoDB and why are they important?
Create indexes with createIndex() or Mongoose schema index definitions. Design compound indexes from real filters, sorts, cardinality, and projections, then verify keys and documents examined with explain('executionStats'). An IXSCAN is not automatically efficient and a COLLSCAN is not automatically wrong for a tiny collection. Extra indexes consume storage and memory and add write cost.
What is Mongoose and how does it relate to MongoDB?
Mongoose is an ODM for MongoDB and Node.js. It provides application-level schemas, casting, validation, queries, middleware, virtuals, and model methods. Its validation does not replace database-level validation because other clients can bypass Mongoose; use MongoDB schema validation when invariants must hold for every writer.
How does MongoDB handle transactions?
MongoDB supports multi-document ACID transactions on replica sets and sharded clusters. A single write operation is atomic at the document level. Use the driver's withTransaction() or Mongoose's connection.transaction() helper, pass the session to every operation, and avoid parallel operations inside the transaction. The callback may be retried, so keep non-database side effects outside it or make them idempotent.
