Dissecting MongoDB's most important data design decision: when to embed documents for data locality and when to reference with ObjectId, mapping the one-to-few, one-to-many, and many-to-many patterns, and the hybrid approach that balances both.

In episode 6 you learned about the principle of data locality and the main anti-patterns. Now we get into the most concrete decision, and the one MongoDB architects face most often: is this data embedded into a document, or referenced to another collection? This is the decision that determines the performance, consistency, and complexity of your application for years to come.
Episode 7 guides you to make this decision with a clear thinking framework. The roadmap: first we understand the embedded documents model (denormalization), second referenced documents (normalization), third we map the one-to-few, one-to-many, and many-to-many relationship types to the right model, and fourth we combine everything in a hybrid approach. Let's get started.
Embedding means storing related data in the same document — as an embedded document or an array of documents. This is MongoDB's default approach because it aligns with the principle of data locality. One query reads all related data without JOINs.
Embedding is best suited for one-to-few relationships: one parent document with a few built-in pieces of data that are small and rarely change. The classic example is a user's addresses:
{
"_id": ObjectId("66xxxxxxxxxxxxxxxxxxxxxxxx"),
"name": "Arman Dwi Pangestu",
"email": "arman@example.com",
"addresses": [
{
"label": "rumah",
"street": "Jl. Merdeka No. 10",
"city": "Jakarta",
"zip": "10110"
},
{
"label": "kantor",
"street": "Jl. Sudirman Kav. 52",
"city": "Jakarta",
"zip": "12190"
}
]
}The advantages of embedding are very clear here:
find.Embedding starts to hurt when the relationship is one-to-many with a large quantity, or when data is frequently updated on its own:
Referencing means storing the relationship using a reference value — usually an ObjectId — that points to a document in another collection. Data isn't stored together; it's connected logically.
Referencing suits one-to-many and many-to-many relationships with large quantities. The classic example is the user and order relationship — one user can have thousands of orders:
{
"_id": ObjectId("66xxxxxxxxxxxxxxxxxxxxxxxx"),
"userId": ObjectId("66yyyyyyyyyyyyyyyyyyyyyyyy"),
"items": [
{ "productId": ObjectId("66zzzzzzzzzzzzzzzzzzzzzzzz"), "qty": 1 }
],
"total": 150000,
"status": "paid",
"createdAt": ISODate("2026-08-03T10:00:00Z")
}The users and orders collections are separate; the order document stores userId as a reference. Its advantages:
The downside: reading an order along with the user's name requires two queries (or one $lookup in aggregation — which we learn in episode 10).
A parent relationship with a few small attached items (addresses, tags, preferences). Example: user and address, post and tags.
There are two variations:
reviewIds. Suitable if you need to know the relationship from the parent side and the quantity is reasonable.userId. Suitable for large quantities because the parent's array doesn't balloon. This is the most common pattern.{
"_id": ObjectId("66xxxxxxxxxxxxxxxxxxxxxxxx"),
"productId": ObjectId("66zzzzzzzzzzzzzzzzzzzzzzzz"),
"rating": 5,
"text": "Produk sangat bagus",
"userId": ObjectId("66yyyyyyyyyyyyyyyyyyyyyyyy")
}Both reference each other. Example: books and authors — one book has many authors, one author has many books. Store an authorIds array in the book document, or bookIds in the author document, or both.
db.books.find({ authorIds: ObjectId("66yyyyyyyyyyyyyyyyyyyyyyyy") })In real practice, the best design is almost always hybrid: combining embedding and referencing based on specific access patterns. The two most common hybrid patterns:
Store a reference for canonical data, but copy a few important fields into the child document as a "snapshot". The most popular example: an order stores productId (reference) while also copying productName and priceAtPurchase (snapshot). If the product price rises in the future, old orders still record the price at purchase time — and order lists can be displayed without a $lookup into the catalog.
{
"_id": ObjectId("66xxxxxxxxxxxxxxxxxxxxxxxx"),
"items": [
{
"productId": ObjectId("66zzzzzzzzzzzzzzzzzzzzzzzz"),
"name": "Laptop Gaming",
"priceAtPurchase": 15000000,
"qty": 1
}
]
}For time-series or log data written very frequently, combine many small records into a single fixed-capacity "bucket" document. For example, storing IoT measurements per hour in one document per hour, containing an array of 60 minutes. The document count shrinks drastically and range queries become fast.
Info
A quick decision framework: ask two things. First, "is this data always read together with the parent document?" If yes and it's small → embed. Second, "does this data grow without limits or change often?" If yes → reference. If the answer is mixed → use hybrid with snapshots. There's no totally wrong answer, only decisions that fit or don't fit the access pattern.
Warning
When embedding, keep in mind the 16 MB maximum document size limit (BSON). Documents that balloon — because of large arrays, excessive snapshots, or accumulating historical data — will run into problems past that limit. Design on the assumption that documents stay lean; data that grows endlessly is always better moved to a separate collection.
In episode 7 you understood the two relationship models in MongoDB. Embedded documents store related data in a single document — perfect for one-to-few like user addresses, with the advantages of one query and data locality. Referenced documents separate data into their own collections with ObjectId references — right for large one-to-many and many-to-many, with the advantages of no duplication and lean documents. And the hybrid approach combines both, using references for canonical data plus snapshots for fast reads.
Key takeaways:
In the next episode, episode 8, we make schema design real and protected: Schema Validation & Data Integrity Rules. You'll use $jsonSchema to define validation rules on collections, understand the strict versus moderate validation levels, and the error versus warn validation actions. See you in episode 8!