MongoDB
When to use
- Designing or reviewing document schemas (embed vs. reference decisions)
- Writing or optimizing aggregation pipelines
- Planning index strategies (compound, partial, text, geospatial, Atlas Search)
- Implementing multi-document ACID transactions
- Configuring sharding keys and cluster topology
- Setting up MongoDB Atlas (clusters, Atlas Search, Atlas Vector Search, Triggers, App Services)
- Migrating from relational models to document model
Workflow
Clarify access patterns first. List every query the application will run, their frequency, and sort/filter fields. Schema design follows access patterns, not entity relationships.
Embed vs. reference decision:
- Embed when: 1-to-few relationship (< ~16 MB doc limit), child data is always read with parent, child has no independent lifecycle, child is not queried standalone.
- Reference when: 1-to-many with unbounded growth, child accessed independently, many-to-many, data shared across multiple parents, or child updates frequently without parent needing reload.
- Hybrid: embed a bounded summary (e.g., last 5 comments) + reference full collection.
Design the schema:
- Keep hot fields at the top level for index efficiency.
- Use sub-documents for cohesive nested data (address, metadata).
- Use arrays sparingly on the reference side of a relationship; unbounded arrays cause document growth and index bloat.
- Add
created_at/updated_atat the top level on every collection. - Avoid polymorphic arrays of mixed types; use a
typediscriminator field if necessary.
Index strategy:
- Single-field index: simple equality filters.
- Compound index: follow ESR rule — Equality fields first, then Sort fields, then Range fields.
- Partial index:
{ partialFilterExpression: { status: "active" } }to index only relevant docs. - Sparse index: when field is absent on most documents.
- TTL index: automatic document expiry (
expireAfterSeconds). - Text / Atlas Search: full-text needs Atlas Search (Lucene-backed) for production; avoid
$textat scale. - Avoid redundant indexes; each write pays for every index. Run
db.collection.getIndexes()and$indexStatsregularly. - Use
.explain("executionStats")to verify IXSCAN vs COLLSCAN and nReturned vs nScanned ratio.
Aggregation pipeline:
- Push
$matchand$limitas early as possible to reduce working set. $matchon an indexed field before$groupis the single biggest performance lever.- Use
$projectto drop unused fields before expensive stages. $lookupis expensive; consider denormalizing if called in hot paths. Usepipelineform of$lookupwith a$matchto limit joined documents.$facetruns multiple sub-pipelines on the same input; good for search + pagination + counts in one round-trip.- Use
allowDiskUse: trueonly for ETL/reporting pipelines, not user-facing queries. - Prefer
$mergeor$outfor materializing aggregation results into summary collections.
- Push
Transactions (multi-document ACID):
- Available on replica sets (default Atlas) and sharded clusters.
- Keep transactions short (< 60 s, ideally < 1 s) — long transactions cause lock contention and oplog pressure.
- Never perform network I/O, external API calls, or user interaction inside a transaction.
- Retry on
TransientTransactionErrorandUnknownTransactionCommitResultusing the official retry loop pattern. - Prefer single-document atomicity (embedded sub-documents) over transactions when the data fits.
Sharding:
- Choose a shard key with high cardinality, low frequency (no hot shard), and query isolation (most queries include the shard key).
- Hashed shard key: good write distribution, bad for range queries.
- Ranged shard key: efficient range queries, risk of hot shards.
- Compound shard key:
{ tenant_id: 1, _id: 1 }for multi-tenant SaaS — isolates per-tenant data. - Once set, shard key cannot be changed (MongoDB 5.0+ allows resharding but it is expensive).
- Pre-split chunks for predictable large initial loads.
Atlas setup:
- Use M10+ for production; M0/M2/M5 are shared, no backups, no VPC peering.
- Enable backup (continuous or snapshot-based) on all production clusters.
- Use VPC peering or Private Link; never expose clusters to the public internet.
- Atlas Search: define index in Atlas UI or IaC (
mongocli/atlasCLI); use$searchaggregation stage. - Atlas Vector Search: store embeddings as
float[]arrays; define vector index withnumDimensionsandsimilaritymetric. - Use Atlas Triggers for change-stream-driven serverless logic rather than polling.
- Connection string: always use SRV format
mongodb+srv://...; setmaxPoolSize,serverSelectionTimeoutMS,connectTimeoutMS.
Driver best practices (Node.js / PyMongo / Motor):
- Create one
MongoClientper process and reuse it; never open a new client per request. - Use
sessionobjects for transactions; always callsession.endSession()in afinallyblock. - Set
w: "majority"write concern for critical writes;w: 0only for fire-and-forget logging. - Use
readPreference: "secondaryPreferred"for analytics/reporting queries to offload primaries.
- Create one
Standards
Do:
- Design schema for the dominant read pattern.
- Use
_idas the shard key suffix for uniqueness guarantees. - Run
explain()before declaring a query fast. - Use connection pooling (default pool size 100 in Node driver).
- Enable
retryWrites: trueandretryReads: truein connection string (Atlas default). - Use Mongoose discriminators for polymorphic collections.
- Store monetary values as integers (cents) or
Decimal128, neverfloat. - Use Atlas Data API or App Services for mobile/browser direct access instead of exposing the driver.
Do not:
- Store large binary files (> 16 MB) in documents; use GridFS or S3 + reference URL.
- Use
$whereor server-side JavaScript — security risk and disables query optimizer. - Create an index on every field "just in case" — write amplification degrades throughput.
- Use
remove()/update()without a filter — drops entire collection / updates all docs. - Use
findOnein a loop; batch with$inor cursor iteration. - Run
mongodumpon Atlas production as a backup strategy; use Atlas Backup instead. - Rely on
ObjectIdsort order as a substitute for acreated_attimestamp — ObjectId encodes seconds, not milliseconds.
Common mistakes to avoid
- Unbounded array growth: embedding all comments/events in a parent document until it hits the 16 MB BSON limit. Solution: reference pattern or bucket pattern.
- Missing compound index: running a query that filters on
statusand sorts oncreated_atwithout a{ status: 1, created_at: -1 }index causes a COLLSCAN + in-memory sort. - Ignoring write concern in transactions:
w: 1in a transaction can lose committed data on primary failure. - Sharding on
_id(ObjectId) alone: monotonically increasing shard key concentrates all writes on the last chunk (hot shard). Use hashed or compound key. - Opening a new MongoClient per Lambda invocation: cold-start overhead and connection pool exhaustion. Cache the client outside the handler.
- Using
$lookupon unindexed foreign field: always index theforeignFieldin the joined collection. - Storing ISO date strings instead of BSON Date: loses timezone math, range query efficiency, and TTL index support.
Output format
- Schema designs →
docs/data-model/(use template.claude/templates/data-model.md) - Aggregation pipelines → inline code blocks in implementation docs under
docs/specs/ - Index plan →
docs/decisions/mongodb-indexes-<feature>.md(use template.claude/templates/decision-record.md) - Atlas configuration →
docs/deployment/atlas-setup.md(use template.claude/templates/deployment-plan.md)
Related checklists
.claude/checklists/data-model.md.claude/checklists/database.md.claude/checklists/performance.md.claude/checklists/security.md
Related agents
.claude/agents/core/database-architect.md.claude/agents/engineering/data-engineer.md.claude/agents/engineering/backend-engineer.md.claude/agents/quality/performance-engineer.md