MongoDB Documents at Scale
MongoDB stores data in the shape your application uses. This page explains the document model and BSON, how to decide between embedding and referencing, the schema design patterns, how indexes and the ESR guideline decide query speed, how to read explain output (with a real run), the aggregation pipeline, replica sets with write and read concern, sharding and shard key choice, transactions, change streams, validation, Spring Data MongoDB and a Docker lab.
Documents, BSON, Collections and ObjectId
MongoDB is a document database. The unit of storage is a document: an ordered set of field/value pairs, written in a JSON-like form and stored in a binary format called BSON. Documents live in collections (roughly tables), and collections live in databases. Unlike a table row, a document can contain arrays and nested documents, so data that a relational design would spread over several tables can be stored, read and written as one unit.
This page targets the MongoDB 8.x line. Where a fact is version-sensitive it says so. All facts were checked against the MongoDB manual on 2026-10-31; the hands-on output comes from a real server run described in chapter 11.
| Relational term | MongoDB term | Notes |
|---|---|---|
| Database | Database | A namespace for collections; also the unit of access control |
| Table | Collection | No fixed columns by default; optional validation rules (chapter 10) |
| Row | Document | Nested objects and arrays allowed; at most 16 MiB (BSON document size limit) |
| Column | Field | Different documents in one collection can have different fields |
| Primary key | _id | Required, unique and immutable; an index on it always exists |
| JOIN | $lookup or embedding | Prefer embedding data that is read together (chapter 3) |
| Transaction | Single-document atomicity, or multi-document transaction | Chapter 10 |
| Type | Example / alias | Gotcha |
|---|---|---|
| String | "shipped" | UTF-8; comparisons are binary unless you set a collation |
| Int32 / Int64 / Double | NumberInt(5), NumberLong(5), 5.5 | In mongosh a plain number literal is a double, which can surprise $jsonSchema bsonType: "int" checks |
| Decimal128 | NumberDecimal("19.90") | Use for money; doubles cannot represent 0.1 exactly |
| Date | ISODate("2026-10-31T10:00:00Z") | Stored as milliseconds since the epoch, always UTC. Store dates as Date, not strings |
| ObjectId | ObjectId() | 12 bytes, explained below |
| Boolean, Null | true, null | A missing field and a field set to null are different to queries and to sparse indexes |
| Array | ["vip", "beta"] | Queries match if any element matches; indexing an array makes a multikey index (chapter 4) |
| Embedded document | { city: "Pune", zip: "411001" } | Query nested fields with dot notation: "address.city" |
| BinData, UUID | UUID("...") | Drivers differ in the legacy UUID representation; set it explicitly in your driver |
If you do not supply _id, the driver generates an ObjectId on the client before sending the insert. It is 12 bytes: a 4-byte timestamp (seconds since the Unix epoch), a 5-byte random value generated once per client process, and a 3-byte incrementing counter initialised to a random value. That makes ids unique without a central sequence, roughly ordered by creation time, and cheap to generate in any application instance.
- The first 4 bytes (
6ae6297b) are the timestamp, the next 5 identify the process, the last 3 are the counter. Ids from one process increase, ids across processes are only ordered to the second. _idcan be any immutable unique value: a string, a number, a UUID, a compound document. A natural key (an order number, an email) is fine if it never changes. You cannot update_id.- Do not treat the timestamp inside an ObjectId as your business "created at". Store a real
createdAtfield if you query by it.
"Schemaless" really means "the database does not force a schema by default". Your application still has one, and every code path that reads the collection depends on it. Without validation (chapter 10) and a versioning habit, a collection ends up with five spellings of the same field and every reader needs defensive code. Decide the shape, validate it, and evolve it deliberately.
Working with Documents in mongosh
mongosh is the MongoDB shell, a JavaScript environment. Every database operation is a method on a collection. The examples below were run against a real server (see chapter 11); shop is the database and orders and users the collections. Collections and databases are created implicitly on first write.
| Operation | Methods | Notes |
|---|---|---|
| Create | insertOne, insertMany | insertMany is ordered by default: the first error stops the batch. Pass {ordered: false} to continue |
| Read | find, findOne, countDocuments | find returns a cursor; the shell prints the first 20 |
| Update | updateOne, updateMany, replaceOne, findOneAndUpdate | Updates use operators ($set, $inc, $push), which are atomic per document |
| Delete | deleteOne, deleteMany | A filter of {} matches everything. Check your filter with find first |
Filters combine with an implicit AND. The most useful operators are the comparison family ($eq $ne $gt $gte $lt $lte $in $nin), logical ($and $or $not $nor), element ($exists, $type), array ($all, $elemMatch, $size) and $regex. Large skip values are slow because the server still walks past the skipped entries; paginate with a range on an indexed field instead (createdAt < lastSeen).
| Operator | Does | Operator | Does |
|---|---|---|---|
$set / $unset | Set or remove a field | $push / $addToSet | Append / append if absent |
$inc / $mul | Add / multiply atomically | $pull / $pop | Remove matching / first or last element |
$min / $max | Keep the lower / higher value | $rename | Rename a field |
$currentDate | Set a Date or timestamp | $[<id>] positional filters | Update specific array elements |
- A write to one document is atomic, including all of its nested fields and array elements.
$incis a safe counter without a transaction. This is why good schema design (chapter 3) keeps data that must change together inside one document.
Every write has a write concern: how many replica set members must acknowledge it before the call returns. The implicit default is w: "majority" (with one arbiter-related exception, chapter 8). You can override it per operation, for example db.orders.insertOne(doc, {writeConcern: {w: 1}}) for a fire-and-mostly-forget write, or {w: "majority", j: true} to be explicit. Chapter 8 explains what these mean and what can still be lost.
updateMany({}, ...)anddeleteMany({})touch every document. Run the same filter throughfind(...).count()orcountDocuments(filter)first, and in applications build filters from validated parameters. Unvalidated user input placed directly in a filter lets a client send operators ({"$ne": null}) instead of values: this is NoSQL injection.
Embed or Reference
In a relational database you normalise first and then write queries. In MongoDB you start from the queries and updates the application performs and shape documents to serve them. The central decision is for each relationship: embed the related data inside the parent document, or reference it by storing its _id.
| Question | Points to embedding | Points to referencing |
|---|---|---|
| Are they always read together? | Yes (order and its lines) | No, the child is often read alone |
| How many children? | Few and bounded (an address, up to dozens of lines) | Many or unbounded (all orders of a customer, all comments of a post) |
| Does the child change independently? | Rarely | Often, or it is updated by another service |
| Is the child shared by many parents? | No, it belongs to one parent | Yes (a product referenced by many orders) |
| Do you need atomic updates across both? | Yes: one document is atomic for free | Needs a multi-document transaction |
| Size of the whole | Stays far below the 16 MiB limit | Parent would grow large, slowing every read and write of it |
| Is the child queried on its own? | Rarely | Often, with its own filters and indexes |
The three classic relationship shapes map to these answers: one-to-one and one-to-few usually embed; one-to-many (thousands) references from the many side, or embeds a small summary (extended reference, below); many-to-many stores arrays of ids on one or both sides.
- Embedding "all comments", "all events" or "all orders" inside a parent document looks convenient until the array reaches thousands of elements. Documents are rewritten and cached whole, so a hot array makes every update heavier, and the 16 MiB limit is a hard stop that fails the write. If a list can grow without a known cap, give its elements their own collection (or use the bucket pattern, chapter 4).
Denormalising a field that changes (a customer name copied into every order) buys read speed at the cost of fan-out writes and stale data. Copy values that are historical facts (the price and name at purchase time) freely; copy values that must stay current only with a plan to update every copy.
Design Patterns for Real Workloads
MongoDB published a series of blog posts called Building with Patterns, and the manual now documents several of them under Schema Design Patterns. They are named solutions to recurring problems. Know the name, the problem and the trade-off; interviewers ask for exactly these.
| Pattern | Problem it solves | How | Trade-off |
|---|---|---|---|
| Bucket | Huge numbers of small records (events, readings, reviews) and slow pagination | Group a series into one document per time span or fixed count, with a cap | Slightly more complex writes. Time series collections apply this automatically and suit most cases |
| Subset | The working set is too big because documents carry rarely used data | Keep the hot part (latest 10 reviews) in the main document, the rest in another collection | Duplicated data; two places to update |
| Extended reference | Frequent $lookup just to show a name or city | Copy the few fields you always display into the referencing document, keep the id | Copies go stale if the source changes; update them, or accept staleness |
| Computed | The same expensive calculation (sum, average, count) on every read | Compute on write or periodically, store the result next to the data | Extra write work; results can lag |
| Outlier | A few documents are far bigger than the rest (a viral book with 1M reviews) | Flag the outlier and move its overflow to a separate document or collection | Application logic for two paths |
| Approximation | Exactness is not worth a write per event (page views) | Update only every N events or by sampling | Not precise, by design |
| Archive | Old data slows hot queries and fills expensive storage | Move cold documents to another collection or storage tier | Queries must know where to look |
| Schema versioning | Documents of several shapes coexist during a migration | Add a schemaVersion field and handle each version in code | Code carries old versions until migrated |
- Pick a pattern because a query is slow, a document is too big, or a hot path needs a join, not because the name sounds good. Measure with
explain(chapter 6) before and after.
Single, Compound, Multikey and Friends
Without an index MongoDB reads every document of the collection (a collection scan, COLLSCAN). An index is a B-tree of key values pointing to documents, kept sorted so equality, range and sort queries can walk a slice. Indexes cost memory and slow every write (each insert must update each index), so you want the few that serve your real queries. A collection can have at most 64 indexes and a compound index at most 32 fields.
| Index type | Create with | Use it for | Notes |
|---|---|---|---|
| Single field | createIndex({email: 1}) | Lookups and sorts on one field | Direction matters little for a single field |
| Compound | createIndex({customerId: 1, status: 1, createdAt: -1}) | Queries on several fields; a prefix of the keys is also usable | Key order is everything; see ESR below |
| Multikey | automatic on an array field | Query array elements (tags: "vip") | One entry per element; a compound index may include at most one array field |
| Unique | {unique: true} | Enforce uniqueness; make upserts safe | Duplicate insert fails with error code 11000 (verified in the lab) |
| Partial | {partialFilterExpression: {status: "open"}} | Index only a subset of documents | Smaller and cheaper; the query filter must imply the expression to use it |
| Sparse | {sparse: true} | Skip documents lacking the field | Partial indexes are the more expressive tool and are generally preferred |
| TTL | {expireAfterSeconds: 3600} on a Date field | Auto-delete sessions, logs, tokens | A background task deletes expired documents, so deletion is not instant |
| Text | createIndex({body: "text"}) | Basic $text word search | Only one text index per collection; for rich search use a search engine (page 05) |
| Hashed / geospatial / wildcard | {id: "hashed"}, 2dsphere, {"$**": 1} | Hashed shard keys, geo queries, unpredictable field names | Wildcard indexes are a tool for flexible shapes, not a substitute for designed indexes |
For a compound index MongoDB recommends ordering keys by Equality, Sort, Range (the diagram below). Equality fields first keep the remaining keys sorted; then the sort field so the index delivers order; then range fields. It is a guideline, not a law: always confirm with explain.
- A covered query is answered from the index alone: every filtered field and every returned field is in one index, and
_idis excluded from the projection (unless it is in the index).explainthen showsdocsExamined: 0. Chapter 6 shows a real one. - Be careful with index intersection: in principle the planner can combine two single-field indexes, but you should not design for it. A purpose-built compound index is almost always faster and more predictable, so create the compound index your query needs.
- Every index must fit comfortably in memory with the hot data, or lookups hit disk. Drop indexes nothing uses (
$indexStatsshows usage).
- An index on
{status: 1, createdAt: -1}does not help a query oncreatedAtalone (it is not a prefix), and low-cardinalitystatusfirst gives poor selectivity for the other keys unless the query always filters it. Indexing every field slows all writes and wastes memory. Start from the query list, apply ESR, and prove it withexplain.
Reading explain("executionStats")
explain shows how the server runs a query. Use the verbosity "executionStats": it actually runs the query and reports counts. The three numbers to read are **nReturned (documents returned), totalKeysExamined (index entries read) and totalDocsExamined** (documents fetched). A healthy query examines about as many keys and documents as it returns.
The run below used a collection of 100,000 orders with 1,000 customers, four statuses and a timestamp. The query is find({customerId: 42, status: "shipped"}).sort({createdAt: -1}), which returns 100 documents. These are real numbers from MongoDB 7.0.45 (the 8.x shell output has the same structure). The ms values are from one run on a laptop and only show direction.
| Index present | Winning plan | nReturned | keysExamined | docsExamined | ms |
|---|---|---|---|---|---|
| none | SORT > COLLSCAN | 100 | 0 | 100000 | 85 |
{customerId: 1} | SORT > FETCH > IXSCAN | 100 | 100 | 100 | 3 |
{customerId: 1, status: 1, createdAt: -1} | FETCH > IXSCAN (no SORT) | 100 | 100 | 100 | 5 |
same, projection {_id:0, createdAt:1, customerId:1, status:1} | PROJECTION_COVERED > IXSCAN | 100 | 100 | 0 | 2 |
same index, but query {status: "shipped"} sorted by createdAt, limit 5 | SORT > COLLSCAN | 5 | 0 | 100000 | 169 |
Read the table as a story. With no index the server examined all 100,000 documents to return 100. The single-field index cut that to 100 keys and 100 documents but still sorted the 100 in memory (the SORT stage). The ESR index made the sort free. The projection made it covered. The last row is the lesson about prefixes: the query does not filter customerId, so the compound index cannot be used and the planner falls back to a collection scan.
| Plan stage | Meaning | Worry when |
|---|---|---|
COLLSCAN | Reads the whole collection | The collection is large and the query is frequent |
IXSCAN | Walks an index range (see indexBounds) | Bounds are [MinKey, MaxKey]: the index is scanned fully |
FETCH | Loads documents the index pointed to | docsExamined is far above nReturned (a second filter runs after fetch) |
SORT | In-memory sort | Large input; sorts have a memory limit and can spill or fail |
PROJECTION_COVERED | Result built from the index only | This is the good case |
SORT_MERGE, OR, SHARD_MERGE | Combine several index scans or shards | Check each branch has its own index |
- Use
explain("queryPlanner")for the plan without running it,db.collection.find().hint({...})to compare a specific index,$indexStatsto find unused indexes, and the profiler or slow query log (slowms, default 100 ms) to discover which queries to explain in the first place.
The Aggregation Pipeline
find filters and projects. When you need grouping, joins, reshaping or computed output, use the aggregation pipeline: an array of stages, where each stage takes the documents from the previous one and passes new documents on. It is MongoDB's answer to GROUP BY, JOIN and subqueries.
| Stage | Does | Notes |
|---|---|---|
$match | Filter documents | First in the pipeline it can use indexes. Same syntax as find |
$project / $addFields / $set | Select, rename, compute fields | The server already prunes unused fields automatically in many cases |
$group | Group by a key and accumulate ($sum, $avg, $min, $max, $push, $addToSet) | Blocks until all input is read; memory heavy for many groups |
$sort, $limit, $skip | Order and page | $sort followed by $limit is optimised into a top-N sort; a $sort on an indexed prefix is free early on |
$unwind | One output document per array element | Multiplies documents; filter first |
$lookup | Left outer join to another collection | Index the foreign field. Works with sharded collections since 5.1 |
$facet | Run several sub-pipelines on the same input (counts plus a page of results) | Returns one document, so its total output is bound by the 16 MiB document limit |
$unionWith, $merge, $out | Combine collections, write results to a collection | Basis for materialised views and ETL |
- Filter and cut early.
$matchand$limitfirst. The optimiser moves some stages for you (for example$matchbefore$sort, or before a$project), but it cannot fix a design that groups everything and filters later. - Check
db.collection.explain("executionStats").aggregate([...])for the real plan, as withfind. - Memory: stages that need more than 100 megabytes write temporary files to disk by default since 6.0 (
allowDiskUseByDefault), which makes a big group slow rather than failing. Pass{allowDiskUse: false}to forbid it per command. - A
$lookupon an unindexed foreign field performs a scan per input document. Index the foreign key, or denormalise with extended reference (chapter 4). - If you aggregate the same big data constantly, precompute (computed pattern) or write a summary collection with
$merge.
- Frequent multi-collection
$lookupchains over big collections suggest that the schema is relational data in a document store. Rework the model (embed or extended reference) before tuning the queries.
WiredTiger, Replica Sets, Write and Read Concern
WiredTiger is the default storage engine. You rarely tune it, but you should know what it does for you.
| Topic | What to know | Version / default |
|---|---|---|
| Concurrency | Document-level concurrency control: writers to different documents do not block each other | Since 3.0 |
| Cache | WiredTiger keeps an internal cache of hot data and indexes; the OS file cache holds more. Do not set the cache above its default without a reason | Default is the larger of 50% of (RAM minus 1 GB) or 256 MB (docs, current) |
| Journal | A write-ahead log: changes are journaled so a crash replays everything since the last checkpoint. j: true waits for the journal flush | Journal is flushed about every 100 ms (storage.journal.commitIntervalMs) and for j: true writes |
| Compression | Block compression (snappy by default) for collections, prefix compression for indexes; journal is compressed too | Alternatives such as zstd are configurable |
| Checkpoints | Periodic consistent snapshots of the data files | Journal fills the gap between checkpoints |
| Transactions | Long open transactions keep old versions alive and put pressure on the cache | See chapter 10 |
A replica set is a group of mongod processes that hold the same data: one primary takes all writes; secondaries copy them. Replication works through the oplog (local.oplog.rs), a capped collection of idempotent operations that secondaries tail and apply. The default oplog size on WiredTiger is 5% of free disk space, between 990 MB and 50 GB; it must cover the time a secondary can be offline and still catch up without a full resync.
Production sets have three or more voting members in different failure domains. An arbiter votes but holds no data; avoid it when you can, because it weakens majority write concern (see the default rule below).
| Setting | Options | Default (current manual) | What it controls |
|---|---|---|---|
Write concern w | 1, a number, "majority", a tag | w: "majority" (implicit default; w: 1 only if the set has an arbiter and data-bearing members are not more than the voting majority) | How many members must have the write before the client is told |
Write concern j | true / false | w: "majority" waits for on-disk journaling by default (writeConcernMajorityJournalDefault) | Wait for the journal flush on the node(s) that count |
Write concern wtimeout | milliseconds | none | Return an error (not a rollback) if the concern is not met in time |
| Read concern | local, available, majority, linearizable, snapshot | local for reads against primary and secondaries | How "committed" the data you read must be |
| Read preference | primary, primaryPreferred, secondary, secondaryPreferred, nearest | primary | Which member serves the read |
localmay return data that is later rolled back.majorityreturns only data acknowledged by a majority, so it survives failover.linearizablegives the strictest single-document reads at a latency cost.snapshotis for transactions.- Reading from secondaries lowers load on the primary but returns data that lags behind, sometimes by a lot. Use it for analytics and reports, not for read-your-writes flows. Causally consistent sessions can give read-your-own-writes across members.
- Unrelated to the above: retryable writes and reads let the driver resend an operation once after a network error or failover, so a single election usually does not surface as an application error.
- A write acknowledged by the primary alone (
w: 1) can be lost if the primary fails before any secondary has it, because that write is rolled back when the old primary rejoins. For data you cannot lose usew: "majority"(already the default) and read withmajoritywhen you need to see only durable data.
Scaling Out with Shards
A replica set copies the same data to every member, so it scales reads and availability but not total data size or write throughput of the primary. Sharding splits a collection across several replica sets (shards). Reach for it when data or write load no longer fits one replica set: not before, because a sharded cluster is much more work to operate.
| Component | Role |
|---|---|
| Shard | A replica set holding a subset of the collection's data (in production each shard is itself a replica set) |
| mongos | Router. Applications connect to it, it looks up which shards own the keys in the query and fans out. Stateless; run several |
| Config servers | A replica set that stores the cluster metadata: which chunk range lives on which shard |
| Chunk / range | A contiguous range of shard key values. Default range size is 128 MB; a chunk is the unit that moves |
| Balancer | Background process that migrates chunks so shards hold similar amounts of data |
You choose a shard key (one field or a compound of fields) per collection. Every query is then either targeted (it includes the key, one shard answers) or scatter-gather (every shard answers and mongos merges). Good keys have high cardinality, even write distribution and match your most frequent query filter.
| Strategy | How it distributes | Good for | Weak spot |
|---|---|---|---|
| Ranged | Contiguous key ranges per chunk | Range queries on the key (createdAt between ...) | A monotonically increasing key (timestamp, ObjectId) sends all new inserts to one shard: a hot shard |
| Hashed | Hash of the key value spreads documents evenly | Even write distribution for ids and monotonic keys | Range queries on the key become scatter-gather |
| Zoned | You tag shard key ranges and pin them to shards | Data locality (EU users on EU shards), tiered hardware | More configuration to maintain |
- Jumbo chunks: a chunk larger than the range size that cannot be split, most commonly because it holds a single shard key value. It cannot be divided or moved easily and becomes a bottleneck. The cure is a higher-cardinality key (add a suffix field).
- Changing the key: since 5.0 you can reshard a collection to a different key (
reshardCollection), and you can refine a key by adding suffix fields. Resharding needs free storage on the recipient shards (the docs give a formula of about twice the collection plus index size spread over the shards) and a brief write block at the end; read the requirements on the reshard page before attempting it. Treat it as an escape hatch, not a plan. - Transactions and
$lookupwork on sharded collections, but cross-shard operations cost more than single-shard ones.
- Sharding
orderson{createdAt: 1}or_id(an ObjectId, ordered by time) makes every insert land in the newest range on a single shard while the others idle. Use a hashed key, or a compound key whose first field spreads the writes ({tenantId: 1, createdAt: 1}).
- A key that your queries do not filter on turns every read into scatter-gather across all shards, which is slower than a single replica set. Confirm you really need sharding, pick the key from the real query mix, and test with representative data.
Transactions, Change Streams and Schema Validation
Since 4.0 (replica sets) and 4.2 (sharded clusters) MongoDB supports multi-document ACID transactions. Inside a transaction, multiple operations on multiple documents and collections commit together or not at all, with snapshot isolation.
- Cost. The manual states that in most cases a distributed transaction costs more than single-document writes and should not replace good schema design: denormalised, embedded models usually avoid the need.
- Limits. A transaction has to finish in under 60 seconds by default (
transactionLifetimeLimitSeconds), otherwise it is aborted. Long transactions hold snapshot history and put pressure on the WiredTiger cache. Since 4.2 a transaction's writes are spread over several oplog entries, so there is no 16 MB total limit, but each entry must still fit the document limit. - Errors. Drivers expose a callback API (
withTransaction) that retries the whole transaction onTransientTransactionErrorand retries the commit onUnknownTransactionCommitResult. Prefer it to hand-written retry logic. - Read preference inside a transaction must be
primary.
| Use a transaction when | Avoid it when |
|---|---|
| Two or more documents must change together (money transfer, inventory plus order) | The data can live in one document (atomic for free) |
| An invariant spans collections and cannot be redesigned away | You are using it to emulate relational joins everywhere |
| Operations are short and touch few documents | The work is a long batch job; chunk it into idempotent steps instead |
A change stream lets an application subscribe to inserts, updates, deletes and DDL events on a collection, database or whole deployment without tailing the oplog by hand. Events come in order, each carries a resume token (the event _id), and a stream can be resumed after a disconnect as long as the token is still inside the oplog window. They need a replica set or sharded cluster.
- Store the resume token after processing so a restarted consumer continues where it stopped.
fullDocument: "updateLookup"fetches the current document at lookup time, which may differ from the document at the time of the change; for the before/after image enable pre- and post-images (changeStreamPreAndPostImages) on the collection.- If the oplog rolls past your token you lose the ability to resume. Size the oplog for your worst consumer outage.
- Typical use: cache invalidation, search indexing, event publishing. For reliable publication to a broker, change streams plus a persisted token play the role of an outbox reader (see Messaging page 07).
Validation runs on writes, so it protects new data but does not check what is already stored. Add rules in warn mode first, fix the log noise, then switch to error. Existing collections can be changed with collMod. Validation is your defence against the "schemaless" trap from chapter 1.
Spring Data MongoDB, Docker Hands-On and Operations
Spring Data MongoDB maps Java classes to documents and offers two styles: the repository interface (MongoRepository) for CRUD and derived queries, and MongoTemplate for everything else (updates with operators, aggregations, bulk operations). Both sit on the MongoDB Java driver. The code on this page was compiled with javac against spring-boot-starter-data-mongodb 3.5.0, which brings Spring Data MongoDB 4.5.0 and the MongoDB Java driver 5.4.0. Spring Boot 4.0.0 brings Spring Data BOM 2025.1.0 and driver 5.6.1 (Spring Data MongoDB 5.x). Check the BOM for the Boot version you use.
| Annotation / type | Purpose |
|---|---|
@Document(collection = "orders") | Marks a class as a MongoDB document and names the collection |
@Id | The _id field. A String id maps to an ObjectId when it is a valid hex string; the field can also be ObjectId, Long, UUID |
@Indexed, @CompoundIndex | Declare indexes. Auto index creation is off by default since Spring Data MongoDB 3.0; set spring.data.mongodb.auto-index-creation=true in development, and create indexes via migrations in production |
@Field("total") | Rename the stored field |
@Version | Optimistic locking: an update with a stale version fails with OptimisticLockingFailureException |
MongoRepository<T, ID> | CRUD, paging, derived queries (findByCustomerIdAndStatus...), @Query with JSON filters |
MongoTemplate | Fluent Query/Criteria/Update API and Aggregation |
MongoTransactionManager | Enables @Transactional against MongoDB; you must declare the bean and the server must be a replica set or sharded cluster |
- The
Decimal128/BigDecimalmapping means money round-trips exactly, but the aggregation$sumover decimals is a decimal sum, so useBigDecimalfields for money, notdouble. - Derived queries become real queries on your indexes:
findByCustomerIdAndStatusOrderByCreatedAtDescis the ESR query from chapter 5, so the matching@CompoundIndexmatters. - Use projections (interface or record projections) in repositories to avoid loading large documents when you need two fields.
@Transactionalmethods get a session bound to the thread. The transaction manager uses the default write concern and read concern of the client unless you configure options.
- If no
MongoTransactionManagerbean exists,@Transactionalis not backed by a MongoDB transaction and the writes are independent. Also, against a standalone server (no replica set) starting a transaction fails. Run a single-node replica set even in development (see the Docker lab below).
The image tag mongo:8 exists on Docker Hub (pulled on 2026-10-31). Important note about the lab. On the machine used to write this page (Linux kernel 7.0), the mongo:8 and mongo:8.0 containers refused to start. The log says: MongoDB cannot start: Linux kernel versions 6.19 and newer has a known incompatibility with this version of MongoDB, referencing SERVER-121912. Version 7.0.45 started fine, so every real output on this page comes from **mongo:7.0 (server 7.0.45)**. On a host with an older kernel mongo:8 is what you want; the commands are identical. Output structure of explain is the same in 8.x, but exact numbers will differ on your machine.
The last line is real output from the 7.0.45 run and confirms the majority default. A host connecting from outside Docker must use a replica set host name it can resolve; the localhost:27017 member address above works for tools running inside the container.
- Load 100,000 orders with
insertManyin batches (customerId = i mod 1000, status cycling over four values, createdAt in one-minute steps). - Run the query of chapter 6 with
explain("executionStats")and print the helper summary after everycreateIndex. Expect a collection scan, then an index scan with an in-memory sort, then the ESR index without a sort stage, then a covered query. - Insert a duplicate into a unique index and read error code 11000; run a two-document transaction; create a validated collection and violate it. All of these were run for this page.
| Method | Idea | Caveat |
|---|---|---|
mongodump / mongorestore | Logical export of BSON; simple and portable | Slow on large data; restoring rebuilds indexes; point-in-time needs the oplog option |
| Filesystem or volume snapshot | Snapshot the data volume (journal on the same volume for consistency) | Fast; for sharded clusters you need coordinated snapshots, which is hard by hand |
| Managed backup (Atlas, Ops Manager, cloud tools) | Continuous or scheduled backups with point-in-time recovery | Paid features; know your recovery time and point |
| Delayed secondary | A member intentionally behind by hours | Helps against operator error; not a backup by itself |
A replica set is availability, not backup: a bad deleteMany replicates to every member. Test restores, and know how far back you can go.
| Mistake | What you see | Fix |
|---|---|---|
| Unbounded arrays in a document | Updates slow down, documents near 16 MiB, writes fail | Separate collection or bucket pattern (chapters 3 and 4) |
| Missing index | COLLSCAN, high docsExamined, CPU spikes under load | explain, then an ESR compound index (chapters 5 and 6) |
| Wrong or monotonic shard key | One hot shard, scatter-gather queries, jumbo chunks | Hashed or compound key matching queries (chapter 9) |
| "Schemaless" with no validation | Mixed types for one field, defensive code everywhere | $jsonSchema validation and a schemaVersion field |
Giant $in lists | Huge query documents, slow planning, scattered index lookups | Model the relationship so the filter is one key or a join collection; batch the ids |
w: 1 for important data | Acknowledged writes vanish after failover | w: "majority" and retryable writes |
| Reading from secondaries for read-your-writes | A user does not see their own change | Read from the primary or use causal consistency |
| Treating a replica set as a backup | A deletion replicates everywhere | Real backups and tested restores |
| Transactions everywhere | High latency, write conflicts, aborted long transactions | Embed so one document is atomic (chapter 3) |
Questions You Should Be Able to Answer
Crisp answers for the questions that come up in interviews on MongoDB.
- Q: When would you choose MongoDB over a relational database?
When the data is naturally document-shaped and read as a unit, the shape varies or evolves, and you need to scale horizontally. Choose relational when you need many ad hoc joins, strong cross-entity constraints and mature reporting tools. MongoDB also supports multi-document transactions, so "no transactions" is no longer a valid reason to reject it. - Q: Embed or reference?
Embed data that is read together, bounded and belongs to one parent. Reference data that is shared, large, unbounded or updated independently. Embedding gives one read and atomic updates; referencing avoids duplication and unbounded documents. - Q: What is the maximum document size and why does it matter?
16 MiB (BSON document size limit). It prevents one document from using excessive RAM or bandwidth, and it makes unbounded embedded arrays a design error. Larger files go to GridFS. - Q: What is in an ObjectId?
12 bytes: a 4-byte timestamp, a 5-byte per-process random value and a 3-byte counter. It is generated by the client, roughly time ordered, and unique without coordination. It is a poor shard key on its own because it increases monotonically. - Q: Explain the ESR rule.
Order compound index keys as Equality, Sort, Range. Equality fields first keep the rest ordered, the sort field next lets the index deliver the order, and range fields last so they do not break the sort. - Q: How do you find out why a query is slow?
Runexplain("executionStats"). ComparenReturnedtototalKeysExaminedandtotalDocsExamined, look forCOLLSCANand in-memorySORT, and add or reorder an index. Use the slow query log or profiler to find the queries. - Q: What is a covered query?
One answered entirely from an index: all filter and returned fields are in the index and_idis excluded unless indexed. Explain shows zero documents examined. - Q: What does the default write concern guarantee?
Since 5.0 the implicit default isw: "majority"(with an arbiter-related exception): the write is on a majority of data-bearing voting members and journaled before acknowledgement, so it survives a primary failure. - Q: What can still be lost with
w: 1?
An acknowledged write that has not yet replicated is rolled back if the primary fails and a secondary without it is elected. - Q: Describe a replica set election.
When secondaries have not heard from the primary withinelectionTimeoutMillis(10 s by default), one calls an election and becomes primary with votes from a majority of voting members. Use an odd number of voters. Retryable writes let drivers ride through it. - Q: Read concern levels?
local(default) may be rolled back,availableis for sharded low-latency reads,majorityreturns majority-committed data,linearizableis the strictest single-document read,snapshotis used in transactions. - Q: How does sharding work and how do you choose a shard key?
Data is split into ranges (chunks) by shard key across shards; mongos routes using config server metadata; the balancer moves chunks. A good key has high cardinality, even write distribution and matches the main query filter. Hashed keys even out writes; ranged keys support range queries; zones pin ranges to shards. - Q: What is a jumbo chunk?
A chunk bigger than the range size that cannot be split, usually because it contains one shard key value. It cannot be balanced; fix with a higher-cardinality shard key. - Q: Can you change a shard key?
Yes, since 5.0 by resharding to a new key, and you can refine a key by adding suffix fields. Resharding needs spare storage and careful planning. - Q: Does MongoDB support ACID transactions?
Yes, multi-document ACID transactions on replica sets (4.0) and sharded clusters (4.2) with snapshot isolation. They cost more than single-document writes, have a 60-second default lifetime and should not replace good schema design. - Q: Name some schema design patterns.
Bucket, subset, extended reference, computed, outlier, approximation, archive, schema versioning. Each solves a specific problem such as huge series, oversized working sets, repeated joins or hot aggregates. - Q: What is the oplog?
A capped collection on each replica set member recording idempotent write operations. Secondaries tail it to replicate, and change streams are built on it. Its size sets how long a secondary can lag or be offline without a full resync. - Q: What are change streams used for?
Real-time notification of data changes: cache invalidation, search index sync, event publishing. They are ordered, resumable via the resume token, and need a replica set or sharded cluster. - Q: How does WiredTiger handle concurrency and durability?
Document-level concurrency, an internal cache plus the OS cache, compression, and a write-ahead journal with periodic checkpoints.j: trueorw: majoritywaits for the journal. - Q: Why can
$matchplacement matter in a pipeline?
A leading$matchcan use an index and reduces documents for every later stage. The optimiser moves some filters earlier, but you should write it first anyway. - Q: How do you make Spring
@Transactionalwork with MongoDB?
Declare aMongoTransactionManagerbean, run against a replica set or sharded cluster, and keep transactions short. - Q: Is a replica set a backup?
No. It protects against node failure; a bad delete replicates. Take real backups (dump, snapshots or a managed service) and test restores.
Cheat Sheet and Recap
| Concept | Remember | Value / default (MongoDB 8.x manual) |
|---|---|---|
| Document size | Hard limit, so no unbounded arrays | 16 MiB (BSON document size) |
| ObjectId | Client-generated, roughly time ordered | 12 bytes = 4 timestamp + 5 random + 3 counter |
| Index limits | Per collection / per compound index | 64 indexes, 32 fields |
| ESR | Compound key order | Equality, Sort, Range |
| Healthy explain | Work close to results | keysExamined and docsExamined near nReturned |
| Default write concern | Majority of data-bearing voting members | w: "majority" (arbiter exception) |
| Default read concern / preference | Reads may be rolled back at local | local / primary |
| Election timeout | Failover starts after silence | 10 s (electionTimeoutMillis) |
| Oplog default size | Defines catch-up window | 5% of free disk, min 990 MB, max 50 GB (WiredTiger) |
| WiredTiger cache | Plus OS file cache | Larger of 50% of (RAM - 1 GB) or 256 MB |
| Chunk / range size | Unit of balancing | 128 MB |
| Transaction lifetime | Keep short | 60 s (transactionLifetimeLimitSeconds) |
| Pipeline memory per stage | Spills to disk by default | 100 MB, allowDiskUseByDefault since 6.0 |
| Spring Boot 3.5 | Starter versions | Spring Data MongoDB 4.5.0, driver 5.4.0 |
- Model for your queries. Embed what is read together and bounded; reference what is shared or unbounded. Patterns (bucket, subset, extended reference, computed, outlier) are named fixes for specific problems.
- Indexes decide performance. Use ESR for compound keys and prove every index with
explain("executionStats"): keys and documents examined should be near documents returned. - Aggregation:
$matchand$limitearly, index the$lookupforeign field, precompute what you read constantly. - Replication gives availability, not backup.
w: "majority"is the default and protects acknowledged writes;localreads can be rolled back; use an odd number of voters. - Sharding is a shard key decision. High cardinality, even writes, matches the main filter. Avoid monotonic keys; resharding exists but is expensive.
- Transactions work but cost. Prefer single-document atomicity through design; keep transactions short.
- Validate and version your schema. MongoDB is not schemaless in practice.
- Spring: declare
MongoTransactionManager, create indexes deliberately, useMongoTemplatefor updates with operators and aggregations.
Checked on 2026-10-31 against the MongoDB manual (current pages for limits, BSON types, write and read concern, read preference, elections, oplog, WiredTiger, journaling, indexes, aggregation, sharding, resharding, transactions, change streams and schema design patterns) and Maven Central for Spring artefacts. The manual now also lists 8.3 and 9.0 release notes; this page targets the 8.x line and none of the facts above were found to differ on the pages read. Real command output comes from MongoDB 7.0.45 in Docker because the 8.x images refuse to start on the author's kernel; plan structure and ratios are the same in 8.x but timings will differ. Sharding commands, change streams and the Spring code against a live server were not run; the Spring classes were compiled but not executed. Index intersection is described qualitatively and was not verified against the manual.
- 05 Elasticsearch - when you need real full-text search and relevance ranking.
- 06 Caching Patterns - putting Redis in front of a database like this one.
- 07 Polyglot Persistence - choosing databases per workload.
- Spring pages - the framework side of the Java examples.