MongoDB Relationships: Embed vs Reference and Visualize in DbSchema
For the developer modeling a second MongoDB collection and deciding whether it should have been a field; embedding, references and virtual relations are explained where they appear.
On this page

Two pieces of related data, and one question: do they belong in the same document or in two collections? MongoDB gives you both forms. Embed the child inside the parent when the child is read with the parent and stays small. Store the parent's id in the child and read it back with a second query when the child is read on its own, shared between parents, or grows without a limit. Neither form is a constraint, because MongoDB checks nothing between collections.
- Introduction to MongoDB
- Installation and database creation
- CRUD operations on documents
- Embedded documents and arrays
- Validation rules that enforce structure
- Visualize MongoDB relationships, embedded against referenced (you are here)
- What is an index in MongoDB?
- Aggregation pipeline explained
The key difference between referencing and embedding
Both forms store the same information. What separates them is which operation you pay for, and the two bills arrive at opposite ends of the application.
Embedding buys you the read. The child data arrives in the same operation as the parent, with no second call and no join, and a write that changes two embedded fields at once is atomic, because MongoDB writes atomically at the level of one document. What it costs is duplication: the same nested values sit in every parent that carries them, and changing one means finding all of them.
Referencing buys you the write and the size. The child lives in its own collection, is updated on its own schedule without touching the parent, and can grow past anything a single document could hold. What it costs is a second read, whether that is a separate query from your application or a $lookup stage inside an aggregation.
| Choose embedding when... | Choose references when... |
|---|---|
| child data is always read with the parent | the related data must be reused elsewhere |
| document growth stays small and predictable | child documents change independently |
| you want fewer read operations | you want to avoid duplication |
| the relationship is tightly coupled | the relationship is large or many-to-many |
Referenced relationships
A referenced relationship works the way a foreign key does in SQL, minus the enforcement. One document holds an _id, and another document stores that value in a field of its own.
// users
{
"_id": ObjectId("64fabcde1234567890abc001"),
"name": "Bob Johnson",
"phone": "0723-731-652"
}
// movies
{
"_id": ObjectId("64fabcde1234567890abc002"),
"title": "Inception",
"genre": "Sci-Fi",
"release_date": "2010-07-16"
}
// reviews
{
"_id": ObjectId("64fabcde1234567890abc003"),
"user_id": ObjectId("64fabcde1234567890abc001"),
"movie_id": ObjectId("64fabcde1234567890abc002"),
"rating": 4,
"comment": "Great movie!"
}
The user_id in the review points at the _id in users, and movie_id points at the _id in movies. MongoDB stores both values and checks neither. Pull the two together at query time with a $lookup stage, which adds an array field holding the documents it matched.
db.reviews.aggregate([
{ $lookup: { from: "users", localField: "user_id", foreignField: "_id", as: "author" } }
])
The one review matches the one user, so author comes back holding that single document:
[
{
_id: ObjectId("64fabcde1234567890abc003"),
user_id: ObjectId("64fabcde1234567890abc001"),
movie_id: ObjectId("64fabcde1234567890abc002"),
rating: 4,
comment: "Great movie!",
author: [ { _id: ObjectId("64fabcde1234567890abc001"), name: "Bob Johnson", phone: "0723-731-652" } ]
}
]
The field named in as is always an array, and a review whose user_id matches nothing comes back with an empty one rather than an error. That is the shape of a dangling reference in MongoDB: not a failure, just [].
Embedded relationships
The other form puts the child documents inside the parent, as a nested object or an array of them. It suits data that is written with the parent and read with it.
Instead of three collections, the review and the part of the user profile the review displays go inside the movie document.
// movies
{
"_id": ObjectId("64fabcde1234567890abc002"),
"title": "Inception",
"genre": "Sci-Fi",
"release_date": "2010-07-16",
"reviews": [
{
"user_name": "Bob Johnson",
"phone": "0723-731-652",
"subscription_plan": "Standard",
"rating": 4,
"comment": "Great movie, but hard to follow."
}
]
}
One read now returns the movie and every review of it, and the user's name and plan come along without a second lookup. The duplication is the visible cost: the same reviewer's phone number and plan are copied into every movie that person has reviewed, so a change of plan means an update against each of those movies.
There is a hard stop as well. A single BSON document may not exceed 16 mebibytes, so an array that gains an entry on every event eventually breaks the write that appends to it. Reviews of one film have a natural ceiling; a login history does not. Where you are moving over from SQL, embedding is denormalization and referencing is the closest thing MongoDB has to foreign-key modeling.
Common MongoDB relationship patterns
Three shapes cover most of what a schema needs, and each one picks a side of the trade above. The cardinality decides for you more often than taste does: one-to-one and bounded one-to-many go inside the parent, and anything unbounded or shared goes into its own collection with an identifier joining the two.
One-to-one
// user document with embedded profile
{
"_id": 1,
"name": "Alice",
"profile": { "age": 28, "city": "London" }
}
A profile belongs to exactly one user and is read with the user, so it goes inside as a nested object rather than into a second collection.
One-to-many
// user document with embedded array of addresses
{
"_id": 1,
"name": "Alice",
"addresses": [
{ "type": "home", "city": "London" },
{ "type": "work", "city": "Manchester" }
]
}
An address list is bounded in practice, which is what makes the array safe. Swap addresses for order history and the same shape becomes the array that grows until the document limit stops it.
Many-to-many
// books and authors using references
// Book
{
"_id": 101,
"title": "MongoDB Basics",
"author_ids": [1, 2]
}
// Author
{
"_id": 1,
"name": "Alice"
}
Neither side can embed the other without duplicating it, so one side keeps an array of identifiers. Resolve it with $lookup when a query needs both, as in MongoDB $lookup and MongoDB aggregation pipelines.
Why visualization matters
The relationships above exist only in the code that writes and reads the documents. A field name in an insert, a $lookup in a report, a foreign identifier in a schema file: nothing collects them in one place, and nobody joining the project can read them off the database. A diagram is where they become one picture.
That picture answers questions the shell answers slowly. Which collections carry a user_id, and which of them still have documents pointing at users that were deleted last quarter. Whether a given collection is referenced by one other or by six, which decides how expensive a change to its _id type would be. Where you embedded and where you referenced, which is the design decision a reader has to reconstruct from insert statements otherwise. Validation rules and indexes are easier to review beside the structure they apply to than in the output of getIndexes.
What breaks when nothing enforces the relationship
MongoDB declares no foreign keys, so nothing keeps the two sides in step. A user_id in a notifications collection can name a user document that was deleted a year ago, and the write that created it raised no error. No constraint refuses it, no cascade cleans up after the delete, and no warning appears when the two collections drift apart.
MongoDB does offer an answer where several documents must change together: transactions across multiple documents, collections and shards, available on replica sets since 4.0 and on sharded clusters since 4.2. The manual is direct about when to reach for them, and says a distributed transaction costs more than a single-document write and should not stand in for schema design. So the everyday answer is the design itself, plus a record of the links that the database does not keep.
Visual relationships with DbSchema
DbSchema keeps that record. It connects to MongoDB and introspects a configurable sample of documents per collection, inferring field names, BSON types, nested objects and arrays from what the sample holds. The diagram therefore approximates what the sampled documents contain, and is never a structure MongoDB enforces. Where a collection carries a validation rule, DbSchema reverse-engineers that rule instead and treats it as the authoritative structure. Creating or editing a collection in DbSchema writes the rule back to the database and to the model file together.
On top of that structure you draw the links MongoDB never declared. Drag the field of one collection onto the field it points at in another, and DbSchema creates a virtual foreign key and draws the connector line between the two boxes. The definition goes into the model file. MongoDB neither declares nor enforces that link, so no constraint is created in the database and the running application sees the database it saw before.

Those lines then do work. The Relational Data Editor opens several collections side by side over the relations you drew, and selecting a record in the parent pane refilters every child pane to the documents whose field values match, cascading as many levels deep as the relations go. Select one review and the user who wrote it appears in the pane beside it; a review whose movie_id matches nothing leaves the movies pane empty, which is how a broken reference shows itself without a query.

The last step is handing the result to someone else. Double-click a collection or a field to write a description, then export the model from Diagram → Export HTML5 or PDF Documentation. The interactive HTML5 documentation opens in any browser, carries the diagram as a vector image, and shows those collection and field comments as mouse-over tooltips. Five things make one chain: the structure inferred from your documents, the validation rule where one exists, the virtual relations, the browsing that walks across them, and the interactive documentation at the end. Other tools cover parts of that chain, and the combination is what makes a MongoDB database readable by someone who has never opened it.
Read the wider design questions in MongoDB schema design, and how diagrams are laid out in the diagram documentation.
Download DbSchema at https://dbschema.com/download.html, connect it to your MongoDB database, reverse-engineer the collections you are modeling, and drag the first relation your documents already imply. Connecting, reverse-engineering and the interactive diagrams are in the free Community Edition. Saving the model file, which is where the virtual relations are kept, the Relational Data Editor and the HTML5 documentation are in Pro.
FAQ
Does MongoDB enforce foreign keys?
MongoDB declares no foreign key between two collections. The section What breaks when nothing enforces the relationship above covers what that costs you and what multi-document transactions do about it.
Can MongoDB support many-to-many relationships?
An array of identifiers on one side is the usual form, and a bridge collection holding one document per pair is the alternative when the pairing itself carries data, such as a role or a date. $lookup resolves either one at query time.
See your MongoDB collections as a diagram
DbSchema samples your documents, infers each collection's fields and nested arrays, and lets you draw the virtual relations MongoDB does not declare. The Community Edition is free.