The Complete MongoDB Developer Guide
The complete MongoDB guide: features, best practices, and design patterns.
Features, Best Practices & Design Patterns
Table of Contents
- Introduction & Core Concepts
- Data Modeling Fundamentals
- Schema Design Patterns
- CRUD Operations
- Indexing Strategies
- The Aggregation Framework
- Transactions
- Replication
- Sharding
- Performance Tuning
- Security Best Practices
- Change Streams & Real-Time Apps
- Driver & Application-Level Best Practices
- Monitoring & Operations
- Anti-Patterns to Avoid
- Checklists
1. Introduction & Core Concepts
MongoDB is a document-oriented, distributed NoSQL database that stores data as BSON (Binary JSON) documents. Understanding its core building blocks is the foundation for everything else in this guide.
1.1 Key Terminology
| MongoDB Term | Relational Equivalent |
|---|---|
| Database | Database |
| Collection | Table |
| Document | Row |
| Field | Column |
| Index | Index |
| Embedded Document | (No direct equivalent — like a nested join) |
_id | Primary Key |
1.2 BSON vs JSON
BSON extends JSON with additional types: ObjectId, Date, Decimal128, Binary, Int32, Int64, Timestamp, Regex. Always prefer BSON-native types over storing values as strings (e.g., store dates as Date, not as ISO strings) — this enables proper sorting, range queries, and reduces storage size.
1.3 The Document Model Philosophy
MongoDB’s core strength is that data that is accessed together should be stored together. This is fundamentally different from relational normalization. The central modeling question is always:
“What does my application query, and how often does it query it?”
Not: “What is the most normalized representation of this data?”
2. Data Modeling Fundamentals
2.1 Embedding vs Referencing
This is the single most important decision in MongoDB schema design.
Embed when:
- Data has a “contains” relationship (Order contains Line Items)
- Child data is always/mostly accessed with the parent
- Child data doesn’t grow unboundedly (avoid unbounded arrays)
- Data is read far more often than it’s updated independently
Reference when:
- Child entities are large or grow without bound (e.g., millions of comments)
- Child data is shared/reused across many parents (e.g., a “Product” referenced by many “Orders”)
- Data is frequently updated independently of the parent
- You need to query the child entity on its own, frequently
// Embedding example — a blog post with comments (bounded, always shown together)
{
_id: ObjectId("..."),
title: "MongoDB Best Practices",
author: "Jane Doe",
comments: [
{ user: "alice", text: "Great post!", date: ISODate("2026-01-10") },
{ user: "bob", text: "Very helpful", date: ISODate("2026-01-11") }
]
}
// Referencing example — Orders referencing a Customer (reused, large collection)
{
_id: ObjectId("..."),
customerId: ObjectId("64f1a2..."),
items: [ { sku: "A100", qty: 2, price: 19.99 } ],
total: 39.98
}
2.2 The 16MB Document Limit
Every BSON document has a hard limit of 16MB. This is a critical constraint that shapes modeling decisions — never design a schema where an array or embedded structure can grow unbounded (e.g., “all events for a user” embedded in the user document).
2.3 Working Set & RAM
MongoDB performs best when the working set (frequently accessed data + indexes) fits in RAM. Model your documents to keep frequently-accessed fields together and avoid bloating documents with rarely-used data (use the Extended Reference Pattern, see below, to keep “hot” documents lean).
2.4 Schema Versioning
Because MongoDB is schema-flexible, always include a schemaVersion field when your application evolves over time:
{ _id: ObjectId("..."), schemaVersion: 2, ... }
This allows the application to migrate documents lazily (on read/write) instead of requiring a big-bang migration.
3. Schema Design Patterns
MongoDB’s official documentation catalogs a set of proven schema design patterns. Knowing these by name lets you communicate design decisions clearly with your team.
3.1 Attribute Pattern
Use when you have many similar fields, only some of which apply to any given document, and you need to search across them.
// Instead of: { color: "red", size: "M", material: "cotton", ... } (many optional fields)
{
name: "T-Shirt",
attributes: [
{ k: "color", v: "red" },
{ k: "size", v: "M" },
{ k: "material", v: "cotton" }
]
}
// Index: { "attributes.k": 1, "attributes.v": 1 }
3.2 Extended Reference Pattern
Duplicate a few frequently-needed fields from a referenced document into the parent to avoid extra lookups (a controlled denormalization).
// Order document embeds only what's needed for display, avoiding a join to "customers"
{
orderId: "ORD-1001",
customer: { id: ObjectId("..."), name: "Jane Doe", email: "jane@x.com" }, // extended reference
items: [...]
}
3.3 Subset Pattern
When a document has a large array (e.g., thousands of reviews) but the app usually needs only the most recent N, keep a small subset embedded and the rest in a separate collection.
{
productId: "P-100",
name: "Wireless Mouse",
recentReviews: [ /* last 10 reviews only */ ]
}
// Full reviews live in a separate "reviews" collection referencing productId
3.4 Computed Pattern
Precompute and store expensive-to-calculate values (sums, counts, averages) rather than recomputing them on every read. Update them via application logic, triggers, or scheduled jobs.
{
productId: "P-100",
totalSold: 15234, // computed/cached
avgRating: 4.6, // computed/cached
reviewCount: 812
}
3.5 Bucket Pattern
Group time-series or streaming data into “buckets” (e.g., one document per sensor per hour) instead of one document per reading. Reduces document count and index overhead dramatically.
{
sensorId: "S-42",
hour: ISODate("2026-08-17T09:00:00Z"),
measurements: [
{ ts: ISODate("2026-08-17T09:00:12Z"), temp: 22.1 },
{ ts: ISODate("2026-08-17T09:00:27Z"), temp: 22.3 }
// up to N per bucket
],
count: 2,
sum: 44.4
}
Note: For heavy time-series workloads, prefer MongoDB’s native Time Series Collections (
db.createCollection("sensors", { timeseries: { timeField: "ts", metaField: "sensorId", granularity: "seconds" } })) over hand-rolled bucketing.
3.6 Outlier Pattern
Handle the rare document that breaks your normal embedding assumptions (e.g., a celebrity account with 50 million followers) by adding a flag and overflowing extra data into a separate collection only for that outlier.
{ userId: "u1", followers: [...], hasOverflow: false }
{ userId: "celebrity1", followers: [...1000 shown...], hasOverflow: true }
// followers 1001+ live in "followers_overflow" collection
3.7 Polymorphic Pattern
Store documents of different but related “shapes” in the same collection, differentiated by a type field — useful when the application queries them together.
{ type: "car", make: "Toyota", doors: 4 }
{ type: "motorcycle", make: "Harley", hasSidecar: false }
// Both in a "vehicles" collection
3.8 Tree / Hierarchy Patterns
For hierarchical data (categories, org charts, comment threads), choose based on access pattern:
- Parent references: store
parentId— good for reading a node’s immediate children. - Child references: store
children: [ids]— good for reading a node’s immediate children too, with different tradeoffs. - Array of ancestors: store
ancestors: [id1, id2, ...]— good for fetching the full path/breadcrumb quickly. - Materialized paths: store
path: ",1,2,6,"— good for regex-based subtree queries. - Nested sets: store
left/rightbounds — good for fast subtree queries but expensive updates.
// Array of Ancestors — fast "get all ancestors" and "get all descendants" queries
{
_id: "electronics.laptops.gaming",
name: "Gaming Laptops",
ancestors: ["electronics", "electronics.laptops"]
}
3.9 Approximation Pattern
For analytics where perfect precision isn’t required (e.g., page view counters), reduce write load by only updating a counter probabilistically (e.g., 1 in 100 writes, then multiply by 100).
3.10 Schema Versioning Pattern
Covered in §2.4 — always tag documents with a version so heterogeneous schema versions can coexist during migrations.
3.11 Single Collection Pattern
Related entity types that are always queried together can live in one collection differentiated by a type discriminator, reducing the number of round trips (similar to polymorphic, but emphasizes cross-entity queries in one collection, e.g., an e-commerce app storing product, category, and review types together for single-query storefront pages).
4. CRUD Operations
4.1 Insert
db.users.insertOne({ name: "Jane", email: "jane@x.com", createdAt: new Date() });
db.users.insertMany([
{ name: "Bob" },
{ name: "Alice" }
], { ordered: false }); // unordered = continues on individual doc errors, often faster
4.2 Query Essentials
// Projection — always project only fields you need
db.users.find({ status: "active" }, { name: 1, email: 1, _id: 0 });
// Comparison operators
db.orders.find({ total: { $gte: 100, $lt: 500 } });
// Logical operators
db.orders.find({ $or: [ { status: "pending" }, { status: "processing" } ] });
// Array queries
db.products.find({ tags: "sale" }); // matches if array contains "sale"
db.products.find({ tags: { $all: ["sale", "new"] } });
db.products.find({ "reviews.rating": { $gte: 4 } }); // dot notation into embedded array
// $elemMatch — when multiple conditions must match the SAME array element
db.products.find({
reviews: { $elemMatch: { rating: { $gte: 4 }, verified: true } }
});
4.3 Update
// Prefer targeted operators over full-document replacement
db.users.updateOne(
{ _id: id },
{ $set: { status: "active" }, $currentDate: { updatedAt: true } }
);
// Increment / array operators
db.products.updateOne({ _id: id }, { $inc: { stock: -1 } });
db.posts.updateOne({ _id: id }, { $push: { comments: newComment } });
db.posts.updateOne({ _id: id }, { $push: { comments: { $each: [c1, c2], $slice: -50 } } }); // cap array size
// Upsert
db.counters.updateOne(
{ _id: "orderId" },
{ $inc: { seq: 1 } },
{ upsert: true }
);
// Bulk writes — batch multiple operations for efficiency
db.orders.bulkWrite([
{ updateOne: { filter: { _id: 1 }, update: { $set: { status: "shipped" } } } },
{ updateOne: { filter: { _id: 2 }, update: { $set: { status: "shipped" } } } },
{ deleteOne: { filter: { _id: 3 } } }
], { ordered: false });
4.4 Delete
db.sessions.deleteMany({ expiresAt: { $lt: new Date() } });
4.5 findAndModify Family
// Atomically fetch-and-update — great for queues, counters, locks
db.jobs.findOneAndUpdate(
{ status: "queued" },
{ $set: { status: "processing", startedAt: new Date() } },
{ sort: { priority: -1 }, returnDocument: "after" }
);
5. Indexing Strategies
Indexes are the single biggest lever for query performance. A missing index turns an O(log n) lookup into an O(n) collection scan.
5.1 Index Types
| Type | Use Case |
|---|---|
| Single field | Simple equality/range queries on one field |
| Compound | Queries filtering/sorting on multiple fields |
| Multikey | Automatically created when indexing an array field |
| Text | Full-text search |
| Geospatial (2dsphere) | Location-based queries |
| Hashed | Even distribution for sharding on a single field |
| Wildcard | Unknown/dynamic field names |
| TTL | Auto-expire documents (sessions, logs, caches) |
| Unique | Enforce uniqueness constraint |
| Partial | Index only a subset of documents matching a filter |
| Sparse | Skip documents missing the indexed field |
5.2 The ESR Rule for Compound Indexes
When building compound indexes, order fields as: Equality → Sort → Range.
// Query: find active orders for a customer, sorted by date, in a price range
db.orders.find({ customerId: id, status: "active", total: { $gt: 50 } })
.sort({ createdAt: -1 });
// Optimal index: Equality(customerId, status) -> Sort(createdAt) -> Range(total)
db.orders.createIndex({ customerId: 1, status: 1, createdAt: -1, total: 1 });
5.3 Covered Queries
A query is “covered” when all requested fields exist in the index itself, avoiding a document fetch entirely.
db.users.createIndex({ email: 1, name: 1 });
db.users.find({ email: "a@x.com" }, { email: 1, name: 1, _id: 0 }); // covered
5.4 Partial Indexes vs Sparse Indexes
Prefer partial indexes over sparse indexes in modern MongoDB — they’re more flexible.
// Only index active orders with total > 0 — smaller, faster index
db.orders.createIndex(
{ customerId: 1 },
{ partialFilterExpression: { status: "active", total: { $gt: 0 } } }
);
5.5 TTL Indexes
db.sessions.createIndex({ lastAccess: 1 }, { expireAfterSeconds: 3600 });
5.6 Index Management Best Practices
- Use
explain("executionStats")before/after adding indexes to validate impact. - Build indexes in the background is now default since MongoDB 4.2 (no more blocking builds by default).
- Avoid over-indexing: each index adds write overhead and RAM pressure. Periodically review with
$indexStatsand drop unused indexes. - Keep the number of indexes per collection reasonable (a common guideline: fewer than ~10-15 for write-heavy collections).
- Use
hint()sparingly, only to override the query planner when you’re certain it chooses suboptimally.
6. The Aggregation Framework
The aggregation pipeline is MongoDB’s tool for complex data transformation, analytics, and reporting.
6.1 Core Stages
db.orders.aggregate([
{ $match: { status: "completed", createdAt: { $gte: ISODate("2026-01-01") } } }, // filter early!
{ $group: {
_id: "$customerId",
totalSpent: { $sum: "$total" },
orderCount: { $sum: 1 },
avgOrder: { $avg: "$total" }
}},
{ $sort: { totalSpent: -1 } },
{ $limit: 10 },
{ $lookup: {
from: "customers",
localField: "_id",
foreignField: "_id",
as: "customer"
}},
{ $unwind: "$customer" },
{ $project: { _id: 0, customerName: "$customer.name", totalSpent: 1, orderCount: 1 } }
]);
6.2 Key Stage Reference
| Stage | Purpose |
|---|---|
$match | Filter documents (use as early as possible!) |
$project | Reshape / include-exclude fields |
$group | Aggregate values (sum, avg, count, etc.) by key |
$sort | Order results |
$limit / $skip | Pagination (careful with $skip at scale — see §10.5) |
$lookup | Left-outer-join to another collection |
$unwind | Flatten an array field into multiple documents |
$addFields / $set | Add or compute new fields |
$facet | Run multiple sub-pipelines in parallel, e.g., for search results + counts |
$bucket / $bucketAuto | Categorize documents into ranges |
$graphLookup | Recursive lookup for hierarchical/graph data |
$merge / $out | Write pipeline results into a collection (materialized views) |
$replaceRoot | Promote a sub-document to top level |
$setWindowFields | Window functions (running totals, rankings) — MongoDB 5.0+ |
6.3 Aggregation Performance Rules
$matchand$sortas early as possible — lets the pipeline use indexes on the initial stages.$projectearly to reduce document size flowing through later stages (unless fields are needed later).- Avoid
$lookupon large unindexed foreign collections — always index theforeignField. - Use
allowDiskUse: truefor large aggregations that might exceed the 100MB memory limit per stage. - Use
$facetto combine “data + count” queries into a single round trip. - Materialize expensive recurring aggregations with
$mergeinto a precomputed collection (a form of the Computed Pattern).
6.4 Window Functions Example (MongoDB 5.0+)
db.sales.aggregate([
{ $setWindowFields: {
partitionBy: "$region",
sortBy: { date: 1 },
output: {
runningTotal: { $sum: "$amount", window: { documents: ["unbounded", "current"] } },
rank: { $rank: {} }
}
}}
]);
7. Transactions
MongoDB supports multi-document ACID transactions since v4.0 (replica sets) and v4.2 (sharded clusters).
7.1 When You Need Them
Because of embedding, single-document atomicity (which MongoDB always guarantees) covers most use cases. Reach for multi-document transactions only when you must atomically update multiple documents across collections — e.g., a bank transfer (debit one account, credit another).
const session = client.startSession();
try {
session.startTransaction({
readConcern: { level: "snapshot" },
writeConcern: { w: "majority" }
});
await accounts.updateOne({ _id: fromId }, { $inc: { balance: -amount } }, { session });
await accounts.updateOne({ _id: toId }, { $inc: { balance: amount } }, { session });
await session.commitTransaction();
} catch (err) {
await session.abortTransaction();
throw err;
} finally {
session.endSession();
}
7.2 Transaction Best Practices
- Keep transactions short-lived (default limit ~60 seconds) — long transactions hold locks and accumulate oplog pressure.
- Design your schema (embedding) to minimize the need for cross-document transactions in the first place.
- Always implement retry logic for
TransientTransactionErrorandUnknownTransactionCommitResultlabeled errors — the drivers expose these labels for exactly this purpose. - Avoid transactions across a huge number of documents; batch or redesign if a transaction touches thousands of docs.
8. Replication
A Replica Set is a group of mongod processes maintaining the same data set, providing high availability and read scaling.
8.1 Topology
- 1 Primary (accepts all writes)
- N Secondaries (replicate from primary via oplog, can serve reads)
- Optional Arbiter (votes in elections, holds no data — avoid in production if possible in favor of real data-bearing nodes)
Recommended minimum: 3 data-bearing nodes (odd number, for clean election majority).
8.2 Write & Read Concerns
// Write Concern — how many nodes must acknowledge a write
db.orders.insertOne(doc, { writeConcern: { w: "majority", wtimeout: 5000 } });
// Read Concern — consistency guarantee for reads
db.orders.find().readConcern("majority");
// Read Preference — which member(s) to read from
db.orders.find().readPref("secondaryPreferred");
| Read Preference | Behavior |
|---|---|
primary (default) | Always read from primary — strongest consistency |
primaryPreferred | Primary if available, else secondary |
secondary | Always read from a secondary |
secondaryPreferred | Secondary if available, else primary |
nearest | Lowest network latency member |
Caution: Reading from secondaries risks replication lag / stale reads. Only use for analytics, reporting, or eventually-consistent use cases.
8.3 Best Practices
- Use
w: "majority"for any write you cannot afford to lose on failover. - Monitor replication lag (
rs.printSecondaryReplicationInfo()). - Distribute replica set members across availability zones/data centers for true fault tolerance.
- Use Hidden members for dedicated backup/analytics workloads without affecting the voting topology visibly to the app.
9. Sharding
Sharding is MongoDB’s horizontal scaling mechanism — distributing data across multiple machines (shards) when a single replica set can no longer handle the data volume or throughput.
9.1 Core Components
- Shard: a replica set holding a subset of the sharded data.
- Config Servers: store cluster metadata (also a replica set).
- mongos: the query router applications connect to; routes operations to the correct shard(s).
9.2 Choosing a Shard Key — The Most Important Decision
A good shard key has:
- High cardinality — many distinct values.
- Even distribution — avoids hot shards.
- Query isolation — ideally, most queries include the shard key so
mongoscan target a single shard instead of broadcasting (“scatter-gather”).
sh.shardCollection("app.orders", { customerId: "hashed" }); // hashed = even distribution
// or a compound shard key for range-based queries with good isolation:
sh.shardCollection("app.events", { tenantId: 1, createdAt: 1 });
9.3 Common Shard Key Anti-Patterns
- Monotonically increasing keys (e.g.,
ObjectId, auto-increment, timestamps) as the sole shard key → all new writes hit the same “last” chunk/shard (“hot shard” problem). - Low-cardinality keys (e.g.,
status: "active"/"inactive") → data can’t distribute evenly, creates jumbo chunks.
Mitigation: use a hashed shard key, or a compound key combining a low-cardinality prefix with a high-cardinality, non-monotonic suffix (this is the essence of the official “Hashed Shard Key” and “Compound Shard Key” patterns).
9.4 Zones (Tag-Aware Sharding)
Route specific data ranges to specific shards — commonly used for geo-based data residency requirements (e.g., EU user data must live on EU-located shards).
10. Performance Tuning
10.1 Use explain() Religiously
db.orders.find({ status: "shipped" }).explain("executionStats");
Look for COLLSCAN (bad — full collection scan) vs IXSCAN (good — index used). Check totalDocsExamined vs nReturned — they should be close.
10.2 Avoid Large Skip Values
$skip for pagination becomes slow at scale because MongoDB must still walk past all skipped documents.
// BAD at scale:
db.posts.find().sort({ _id: -1 }).skip(100000).limit(20);
// GOOD — range-based (keyset) pagination:
db.posts.find({ _id: { $lt: lastSeenId } }).sort({ _id: -1 }).limit(20);
10.3 Projection Discipline
Never find() a full document if you only need 2 fields — reduces network transfer and memory pressure.
10.4 Connection Pooling
Reuse a single MongoClient instance per application process; drivers manage a connection pool internally. Do not create a new client per request.
10.5 Avoid $where and Heavy JavaScript
$where and mapReduce execute JavaScript server-side and cannot use indexes efficiently — prefer aggregation pipeline operators.
10.6 Batch Reads and Writes
Use bulkWrite, cursor batching (batchSize()), and insertMany to reduce round-trips.
10.7 Schema Validation
Use JSON Schema validation at the collection level to catch bad data early without sacrificing flexibility:
db.createCollection("orders", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["customerId", "total", "status"],
properties: {
total: { bsonType: "decimal", minimum: 0 },
status: { enum: ["pending", "shipped", "delivered", "cancelled"] }
}
}
},
validationLevel: "moderate" // don't break existing docs during migration
});
11. Security Best Practices
- Enable Authentication & Authorization (
--auth) — never run production without it. - Role-Based Access Control (RBAC) — grant least-privilege custom roles instead of
readWriteAnyDatabase/rootfor application users. - Encrypt in transit — enable TLS/SSL for all client-server and intra-cluster traffic.
- Encrypt at rest — use the WiredTiger encrypted storage engine or disk-level encryption.
- Network isolation — bind to private IPs, use firewalls/VPC security groups, never expose MongoDB directly to the public internet (
0.0.0.0/0bind is a classic breach vector). - Field-Level / Client-Side Field Level Encryption (CSFLE) for highly sensitive fields (PII, payment data) — data is encrypted before it ever leaves the app.
- Audit logging — track access to sensitive collections (Enterprise/Atlas feature).
- Rotate credentials regularly, use secrets managers (never hardcode connection strings with passwords).
- Validate & sanitize all user input — although MongoDB isn’t vulnerable to SQL injection, it IS vulnerable to NoSQL/operator injection if you pass raw user JSON into queries.
// VULNERABLE — user input directly used as an operator
db.users.find({ password: req.body.password }); // if body.password = {"$ne": null} => bypass!
// SAFE — validate types before querying
if (typeof req.body.password !== "string") throw new Error("Invalid input");
12. Change Streams & Real-Time Apps
Change streams let applications subscribe to real-time data changes without polling.
const changeStream = db.collection("orders").watch([
{ $match: { operationType: { $in: ["insert", "update"] } } }
]);
changeStream.on("change", (change) => {
console.log("Order changed:", change.documentKey, change.updateDescription);
});
Use cases: cache invalidation, triggering notifications, syncing to search engines (Elasticsearch/Atlas Search), microservice event pipelines (outbox-less CDC).
Best practices:
- Persist the
resumeTokenso consumers can resume after a disconnect without missing events. - Use
fullDocument: "updateLookup"when you need the complete post-update document, not just the diff.
13. Driver & Application-Level Best Practices
- One
MongoClientper application — it’s thread-safe and manages pooling; avoid re-instantiating per request. - Always set sensible timeouts (
serverSelectionTimeoutMS,connectTimeoutMS,socketTimeoutMS) to fail fast rather than hang. - Use official drivers and keep them updated — they track server capabilities and protocol changes.
- Retry idempotent writes — enable
retryWrites=true(default in modern drivers) to handle transient network blips automatically. - Use an ODM/ORM thoughtfully (Mongoose for Node, Motor for Python async, Spring Data MongoDB for Java) — they add schema validation and lifecycle hooks, but understand what queries they generate under the hood.
- Close cursors properly and avoid loading huge result sets into memory — use streaming/cursor iteration instead of
.toArray()on unbounded queries. - Environment-specific connection strings — never hardcode
mongodb://URIs with credentials in source code; use environment variables or a secrets manager.
14. Monitoring & Operations
db.currentOp()— inspect currently running operations, find long-running or blocked queries.db.serverStatus()— high-level server health (connections, memory, opcounters).$indexStats— see which indexes are actually being used.- Atlas Performance Advisor /
mongodslow query log (db.setProfilingLevel(1, { slowms: 100 })) — find slow queries automatically. - Backups — use continuous backups (Atlas) or
mongodump/filesystem snapshots for self-hosted; always test restore procedures. - Capacity planning — monitor disk usage, WiredTiger cache hit ratio, and connection counts proactively.
15. Anti-Patterns to Avoid
| Anti-Pattern | Why It’s Bad | Fix |
|---|---|---|
| Unbounded arrays in a document | Hits 16MB limit, degrades performance well before that | Subset Pattern / separate collection |
| Massive number of collections (one per user/tenant) | Metadata & storage overhead, hard to manage | Single collection with a tenant/discriminator field |
| Deeply nested documents (>3-4 levels) | Hard to query/index, awkward updates | Flatten or reference |
Using $where/JS for business logic | Slow, can’t use indexes | Aggregation pipeline |
| No indexes on frequently filtered/sorted fields | Full collection scans | Add appropriate compound indexes |
| Storing large binary blobs (images/videos) in documents | Bloats working set, slows queries | GridFS or external object storage (S3) + store the URL |
| Ignoring write concern on critical writes | Silent data loss on failover | w: "majority" |
| Treating MongoDB like a relational DB (over-normalizing) | Excess $lookups, poor performance | Embed per access patterns |
| Skip-based pagination at scale | O(n) cost per page | Range/keyset pagination |
| Not handling transient transaction errors | App-level failures on transient blips | Retry with driver-provided error labels |
16. Checklists
16.1 New Collection Checklist
- Defined access patterns before designing schema
- Decided embed vs reference per relationship
- Added
schemaVersionfield - Added JSON Schema validator
- Created indexes matching top queries (ESR rule)
- Considered array growth limits (16MB document cap)
16.2 Pre-Production Checklist
- Authentication + RBAC roles configured
- TLS enabled
- Appropriate write/read concerns set per operation criticality
- Replica set with ≥3 data-bearing nodes
- Backups configured and restore-tested
- Slow query profiling enabled
- Connection pooling & timeouts configured in driver
- Load-tested query patterns with
explain()
16.3 Scaling Checklist (Before Sharding)
- Confirmed vertical scaling / better indexing isn’t sufficient first
- Chosen shard key with high cardinality & even distribution
- Verified query patterns include the shard key where possible
- Config servers & mongos routers properly sized
- Tested chunk migration behavior under load
This guide covers the practical core of MongoDB development. For deep dives into specific areas (Atlas Search, Vector Search, Time Series Collections, Client-Side Field Level Encryption internals), consult the official MongoDB documentation, as features and best practices continue to evolve.