The Complete MongoDB Developer Guide

The complete MongoDB guide: features, best practices, and design patterns.

🌱 Seedling·created: ·category:Databases

Features, Best Practices & Design Patterns


Table of Contents

  1. Introduction & Core Concepts
  2. Data Modeling Fundamentals
  3. Schema Design Patterns
  4. CRUD Operations
  5. Indexing Strategies
  6. The Aggregation Framework
  7. Transactions
  8. Replication
  9. Sharding
  10. Performance Tuning
  11. Security Best Practices
  12. Change Streams & Real-Time Apps
  13. Driver & Application-Level Best Practices
  14. Monitoring & Operations
  15. Anti-Patterns to Avoid
  16. 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 TermRelational Equivalent
DatabaseDatabase
CollectionTable
DocumentRow
FieldColumn
IndexIndex
Embedded Document(No direct equivalent — like a nested join)
_idPrimary 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/right bounds — 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

TypeUse Case
Single fieldSimple equality/range queries on one field
CompoundQueries filtering/sorting on multiple fields
MultikeyAutomatically created when indexing an array field
TextFull-text search
Geospatial (2dsphere)Location-based queries
HashedEven distribution for sharding on a single field
WildcardUnknown/dynamic field names
TTLAuto-expire documents (sessions, logs, caches)
UniqueEnforce uniqueness constraint
PartialIndex only a subset of documents matching a filter
SparseSkip 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 $indexStats and 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

StagePurpose
$matchFilter documents (use as early as possible!)
$projectReshape / include-exclude fields
$groupAggregate values (sum, avg, count, etc.) by key
$sortOrder results
$limit / $skipPagination (careful with $skip at scale — see §10.5)
$lookupLeft-outer-join to another collection
$unwindFlatten an array field into multiple documents
$addFields / $setAdd or compute new fields
$facetRun multiple sub-pipelines in parallel, e.g., for search results + counts
$bucket / $bucketAutoCategorize documents into ranges
$graphLookupRecursive lookup for hierarchical/graph data
$merge / $outWrite pipeline results into a collection (materialized views)
$replaceRootPromote a sub-document to top level
$setWindowFieldsWindow functions (running totals, rankings) — MongoDB 5.0+

6.3 Aggregation Performance Rules

  1. $match and $sort as early as possible — lets the pipeline use indexes on the initial stages.
  2. $project early to reduce document size flowing through later stages (unless fields are needed later).
  3. Avoid $lookup on large unindexed foreign collections — always index the foreignField.
  4. Use allowDiskUse: true for large aggregations that might exceed the 100MB memory limit per stage.
  5. Use $facet to combine “data + count” queries into a single round trip.
  6. Materialize expensive recurring aggregations with $merge into 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 TransientTransactionError and UnknownTransactionCommitResult labeled 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 PreferenceBehavior
primary (default)Always read from primary — strongest consistency
primaryPreferredPrimary if available, else secondary
secondaryAlways read from a secondary
secondaryPreferredSecondary if available, else primary
nearestLowest 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:

  1. High cardinality — many distinct values.
  2. Even distribution — avoids hot shards.
  3. Query isolation — ideally, most queries include the shard key so mongos can 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

  1. Enable Authentication & Authorization (--auth) — never run production without it.
  2. Role-Based Access Control (RBAC) — grant least-privilege custom roles instead of readWriteAnyDatabase/root for application users.
  3. Encrypt in transit — enable TLS/SSL for all client-server and intra-cluster traffic.
  4. Encrypt at rest — use the WiredTiger encrypted storage engine or disk-level encryption.
  5. Network isolation — bind to private IPs, use firewalls/VPC security groups, never expose MongoDB directly to the public internet (0.0.0.0/0 bind is a classic breach vector).
  6. Field-Level / Client-Side Field Level Encryption (CSFLE) for highly sensitive fields (PII, payment data) — data is encrypted before it ever leaves the app.
  7. Audit logging — track access to sensitive collections (Enterprise/Atlas feature).
  8. Rotate credentials regularly, use secrets managers (never hardcode connection strings with passwords).
  9. 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 resumeToken so 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

  1. One MongoClient per application — it’s thread-safe and manages pooling; avoid re-instantiating per request.
  2. Always set sensible timeouts (serverSelectionTimeoutMS, connectTimeoutMS, socketTimeoutMS) to fail fast rather than hang.
  3. Use official drivers and keep them updated — they track server capabilities and protocol changes.
  4. Retry idempotent writes — enable retryWrites=true (default in modern drivers) to handle transient network blips automatically.
  5. 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.
  6. Close cursors properly and avoid loading huge result sets into memory — use streaming/cursor iteration instead of .toArray() on unbounded queries.
  7. 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 / mongod slow 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-PatternWhy It’s BadFix
Unbounded arrays in a documentHits 16MB limit, degrades performance well before thatSubset Pattern / separate collection
Massive number of collections (one per user/tenant)Metadata & storage overhead, hard to manageSingle collection with a tenant/discriminator field
Deeply nested documents (>3-4 levels)Hard to query/index, awkward updatesFlatten or reference
Using $where/JS for business logicSlow, can’t use indexesAggregation pipeline
No indexes on frequently filtered/sorted fieldsFull collection scansAdd appropriate compound indexes
Storing large binary blobs (images/videos) in documentsBloats working set, slows queriesGridFS or external object storage (S3) + store the URL
Ignoring write concern on critical writesSilent data loss on failoverw: "majority"
Treating MongoDB like a relational DB (over-normalizing)Excess $lookups, poor performanceEmbed per access patterns
Skip-based pagination at scaleO(n) cost per pageRange/keyset pagination
Not handling transient transaction errorsApp-level failures on transient blipsRetry 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 schemaVersion field
  • 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.

This note is part of the Digital Garden — a collection of connected, evolving thoughts.