Filter concepts by levelShowing all levels.

MongoDB · Section 3

Documents and Collections

Level
intermediate
Read
22 min
Concepts
6

The embed-or-reference decision, dot notation for reaching into nested fields, the two shapes an array can take, how document shape affects performance, and what a collection can carry beyond just a name.

MongoDB overview

What is true here

  1. Embed data that is read together, bounded and owned by one parent; reference data that grows independently or is shared.
  2. "a.b" dot notation reaches a nested field the same way in queries, projections and update operators.
  3. A scalar array holds plain values; a document array holds structured, multi-field items.
  4. Large or unbounded arrays and deep nesting cost more to index and update — keep hot documents small.
  5. A collection groups documents by access pattern, not by mirroring the application's class hierarchy.

What you will be able to do

  • Decide between embedding and referencing for a given relationship, and justify the choice
  • Read and write a nested field with dot notation, in a query, a projection, and an update
  • Choose between a scalar array and a document array for a given kind of data
  • Explain how an unbounded array or deep nesting affects update and index cost
  • Explain why a collection is not required to mirror the application's class model

Shaping a document

The embed-or-reference decision, how to reach a nested field, and the two shapes an array can take.

Embedded documents vs. references

coreintermediate

Embedding puts related data directly inside a document; referencing stores just an _id and looks the related document up separately. Embed what is read together and bounded; reference what grows independently or is shared broadly.

Think of it as

Embedding is writing a note directly on the form it belongs to; referencing is writing a case number on the form and keeping the details in a separate file. The note is faster to read together with the form — but if that same note needs to be found from ten other forms too, a shared file referenced by number is easier to keep in sync.

json
// Embed: { "parent": { "child": { ... } } }
// Reference: { "parent": { "childId": ObjectId("...") } }

What we're doing: Model the same relationship (book and author) two ways and show why one book-per-author changes the right choice.

embed-vs-reference.jsonjson
// If an author writes exactly one book, and it's never queried alone:
{ "_id": 1, "title": "Dune", "author": { "name": "Frank Herbert" } }   // embed

// If an author writes many books, and the author is looked up on its own:
{ "_id": 1, "title": "Dune", "authorId": ObjectId("65f1...") }         // reference
{ "_id": "65f1...", "name": "Frank Herbert", "bio": "..." }            // separate document
2
Embedding is right here — the author data is small, read with the book every time, and not shared.
5
Referencing is right here — the same author document is reused across many books, so embedding would duplicate and desynchronize it.

Why this works: The relationship (book has an author) does not change — what changes is cardinality and sharing. Embedding a value that is actually shared across many parents means every copy has to be updated in lockstep, which referencing avoids by keeping one copy.

Embedding data that is shared across many parent documents

Wrong

json
// Author embedded in every one of their 50 books
{ "_id": 1, "title": "Book A", "author": { "name": "Jane Doe", "bio": "..." } }
{ "_id": 2, "title": "Book B", "author": { "name": "Jane Doe", "bio": "..." } }
// updating the bio now means updating all 50 books

Better

json
// Author referenced once, updated in one place
{ "_id": 1, "title": "Book A", "authorId": ObjectId("...") }
{ "_id": 2, "title": "Book B", "authorId": ObjectId("...") }

What you see: A single author bio update requires a multi-document write across every book by that author, and a missed one leaves stale, inconsistent copies.

Why: Embedding trades a join for duplication — that trade only pays off when the embedded data belongs to one parent. Data genuinely shared across many parents needs one source of truth, which is what a reference provides.

Embed vs. reference

Embed

  • +Nested inline, no join
  • +Read together, bounded
  • +Owned by one parent

Reference

  • Stores an _id, needs a lookup
  • Grows independently or shared
  • Has its own lifecycle
  • Embed
    • Nested inline, no join
    • Read together, bounded
    • Owned by one parent
  • Reference
    • Stores an _id, needs a lookup
    • Grows independently or shared
    • Has its own lifecycle

Embed or reference — the deciding questions

Embed or reference — the deciding questions
QuestionFavors embeddingFavors referencing
Read together?yes, almost alwaysrarely, or only sometimes
Bounded size?yes, small and cappedno, or unbounded
Shared by others?no, belongs to one parentyes, reused across documents
Independent lifecycle?no, dies with the parentyes, updated/deleted on its own

Together

json
// Embedded: order lines belong only to this order, read together, bounded
{ "_id": 1, "lines": [{ "sku": "A1", "qty": 2 }] }

// Referenced: the customer is shared across many orders, has its own lifecycle
{ "_id": 1, "customerId": ObjectId("65f1...") }

Remember: Embed data that is read together, bounded and owned by one parent. Reference data that grows independently or is shared across many parents.

See also: document oriented · nested fields and dot notation

Nested fields and dot notation

coreintermediate

Dot notation ("address.city") reaches into an embedded document or an array element without pulling the whole document apart in application code — it works the same way in queries, projections, and update operators.

Think of it as

Dot notation is a path, the same idea as a file system path — "address.city" walks into the address field, then into its city field, one step at a time. Queries, projections and updates all understand this path the same way, so learning it once covers all three.

text
field.nestedField          // embedded document field
field.0.nestedField        // array element at index 0, then its field

What we're doing: Show that $set with a dot path edits only the named nested field, unlike replacing the whole embedded document.

dot-notation-update.txttext
// before: { "_id": 1, "address": { "city": "Delhi", "zip": "110001" } }

updateOne({ _id: 1 }, { $set: { "address.city": "Pune" } })
// -> { "_id": 1, "address": { "city": "Pune", "zip": "110001" } }   zip survives

updateOne({ _id: 1 }, { $set: { "address": { "city": "Pune" } } })
// -> { "_id": 1, "address": { "city": "Pune" } }                    zip is gone
1
Starting document has both city and zip nested under address.
5
Setting the whole "address" field to a new object replaces it entirely — this is the same trap update-vs-replace teaches, one level deeper.

Why this works: A dot path targets exactly the leaf field named — everything else in the embedded document is left as-is. Setting the parent field itself, instead of a dotted child path, replaces the whole embedded document, the same way replaceOne replaces a whole top-level document.

Setting the parent object instead of the specific nested field

Wrong

text
$set: { "address": { "city": "Pune" } }   // silently drops every other field under address

Better

text
$set: { "address.city": "Pune" }   // touches only city, leaves zip and anything else intact

What you see: Sibling fields inside the embedded document disappear after an update that was only meant to change one of them.

Why: $set assigns whatever value is given to the exact path named — a dotted path names one leaf field; the bare parent field name names the whole embedded document.

Walking a dot path one step at a time

document

the whole record

address

"address."

city

"address.city"

  1. document — the whole record
  2. address — "address."
  3. city — "address.city"

Dot notation across the three places it is used

Dot notation across the three places it is used
WhereSyntaxWhat it does
Query filterdb.c.find({ "address.city": "Delhi" })matches nested field by value
Projectiondb.c.find({}, { "address.city": 1 })returns only that nested field
Update ($set)$set: { "address.city": "Pune" }sets only that nested field
Array element"items.0.sku"reaches a specific index inside an array

Together

json
// document: { "_id": 1, "address": { "city": "Delhi", "zip": "110001" } }

db.users.find({ "address.city": "Delhi" })
db.users.updateOne({ _id: 1 }, { $set: { "address.city": "Pune" } })
// -> address.zip is untouched; only address.city changes

Remember: "a.b" reaches field b nested in a, in queries, projections and $set. $set with a dotted path touches only that field, not its siblings.

See also: embedded vs references · arrays of scalars and documents

Arrays of scalars vs. arrays of documents

standardintermediate

An array field can hold plain scalars (tags: ["a", "b"]) or embedded documents (lines: [{ sku, qty }]). Scalars suit a simple set of values; documents suit structured items that themselves have multiple fields.

Think of it as

A scalar array is a short list on a sticky note — just values, nothing more to say about each one. A document array is a list of small forms — each entry has its own set of fields, worth naming and querying individually.

json
{ "scalarArray": ["a", "b"], "documentArray": [{ "field": "value" }] }

Choosing the array shape

Choosing the array shape
DataArray shapeExample
Tags, categoriesscalars"tags": ["sci-fi", "classic"]
Order line itemsdocuments"lines": [{ "sku": "A1", "qty": 2 }]
Simple list of IDsscalars (ObjectId)"followerIds": [ObjectId("..."), ...]
Comments with author + textdocuments"comments": [{ "author": "...", "text": "..." }]

Together

json
{
  "_id": 1,
  "tags": ["sci-fi", "classic"],
  "lines": [
    { "sku": "A1", "qty": 2 },
    { "sku": "B2", "qty": 1 }
  ]
}

Remember: Scalar arrays hold plain values; document arrays hold structured items with their own fields. Both can be indexed as multikey.

See also: nested fields and dot notation · document shape and performance

Advertisement

Shaping a collection

How document shape affects performance, what a collection can carry beyond a name, and when it should mirror a business concept instead of a class.

Document shape and performance

standardintermediate

A document's shape is not just a modeling choice — a large unbounded array slows updates on that document, a deeply nested field is more expensive to index, and a document that must be rewritten often should stay small.

Think of it as

A document is one unit of work for a write: touching any part of it (even one field deep inside a large array) means MongoDB handles the document as a whole. A shape that keeps frequently-updated documents small and stable keeps that unit of work cheap; a shape that piles a growing history into one document makes every future write to it costlier.

text
// No syntax to run — a design constraint to weigh when choosing document shape, not an API call.

Shape choices and their performance effect

Shape choices and their performance effect
Shape choiceEffect
Small, bounded embedded arraycheap to update and index
Large or unbounded embedded arrayupdate cost grows with the array; risks the 16 MB cap
Deeply nested field, frequently queriedindex still works, but the path is costlier to maintain
Frequently updated large documenteach write rewrites more data than a small document would

Together

text
// A "comments" array that grows without bound on a "post" document
// eventually makes every post update touch a large, ever-growing array —
// see schema design patterns (later) for the subset/bucket patterns that fix this.

Remember: Large or unbounded arrays and deep nesting cost more to update and index. Keep frequently-updated documents small and bounded.

See also: embedded vs references · arrays of scalars and documents

Collection-level concerns

standardintermediate

A collection is more than a grouping — it can carry its own validators, indexes, and a special storage mode (capped or time-series) chosen when it is created, each shaping how documents inside it are stored or constrained.

Think of it as

A plain collection is a folder with no rules. A validator adds a form template to the folder. Indexes add a lookup card catalog. Capped and time-series modes change the folder's physical shape entirely — fixed-size and overwrite-oldest, or optimized for append-only timestamped entries.

text
db.createCollection(name, { capped, size, validator, timeseries, ... })

Collection-level options

Collection-level options
ConcernWhat it doesChosen
Validatorrejects (or warns on) documents that fail a schemaat creation, or added later
Indexesspeed up specific query patternsat any time
Capped collectionfixed size, overwrites oldest on overflowat creation only
Time-series collectionoptimized storage for timestamped measurementsat creation only

Together

text
db.createCollection("logs", { capped: true, size: 1000000 })
db.createCollection("readings", { timeseries: { timeField: "ts", metaField: "sensorId" } })
db.createCollection("orders", { validator: { $jsonSchema: { required: ["status"] } } })

Remember: A collection can carry validators, indexes, or a fixed storage mode (capped, time-series) — chosen mostly at creation time.

See also: schema flexible not free · collections as business concepts

Collections as business concepts

standardintermediate

A collection is best named and shaped around a business concept an access pattern actually needs — "orders", "events" — not a 1:1 mirror of every class in the application's object model.

Think of it as

An application's class diagram answers "how is the code organized"; a collection answers "what gets read and written together." The two questions have different right answers — a class hierarchy with five subtypes might still be one collection with a type field, if that is how the data is actually queried.

text
// Ask: what is read/written together, not: what class does this belong to.

Class model vs. collection model

Class model vs. collection model
ConcernApplication class modelCollection model
Organizing principleinheritance, code structureaccess pattern, what's queried together
Polymorphismoften one class per subtypeoften one collection, a type field distinguishes
Granularityas fine as the domain modelas coarse as the read/write pattern needs

Together

text
// Code: CreditCardPayment, BankTransferPayment, WalletPayment classes
// Collection: one "payments" collection, distinguished by a "method" field
{ "_id": 1, "method": "creditCard", "last4": "4242" }
{ "_id": 2, "method": "bankTransfer", "iban": "..." }

Remember: Shape a collection around what is read/written together, not around the application's class hierarchy — polymorphic types can share one collection.

See also: collection level concerns · naming and flexible schema

Advertisement