MongoDB doesn't have foreign keys or native JOIN syntax like relational databases, but it still needs ways to represent connections between data â a customer and their orders, a post and its comments, an employee and their manager. This tutorial covers every relationship style available, from embedding to $graphLookup, and when to reach for each one.
Information
đ 1. Introduction
Relationships in MongoDB come down to a single fundamental choice: should related data live inside the same document, or in a separate document connected by a reference? Every technique in this tutorial is a variation on that theme.
đ§ 2. Understanding Relationships
Choosing a relationship style depends on three questions: how often the related data is accessed together, how large the related data grows, and whether it needs to be queried independently.
Tip
đĻ 3. Embedded Relationships
Embedding nests related data directly inside the parent document, allowing it to be fetched in a single read with no join required.
An embedded relationship
{
_id: ObjectId("..."),
name: "Alice",
billingAddress: {
street: "12 MG Road",
city: "Chennai",
zip: "600001"
}
}Tip
đ 4. Referenced Relationships
Referencing stores a related document's _id instead of its full contents, requiring a separate query or $lookup to resolve it.
A referenced relationship
// orders collection
{
_id: ObjectId("o1"),
customerId: ObjectId("c1"),
amount: 59.99
}Tip
1ī¸âŖâī¸1ī¸âŖ 5. One-to-One Relationships
Best modeled by embedding in most cases, unless the related data is large or infrequently accessed â for example, a rarely viewed user preferences document.
Referencing a rarely accessed one-to-one relationship
// Referenced alternative for large or rarely accessed data
// users collection
{ _id: ObjectId("u1"), name: "Alice" }
// user_preferences collection
{ _id: ObjectId("p1"), userId: ObjectId("u1"), theme: "dark", notifications: {} }1ī¸âŖâī¸đĸ 6. One-to-Many Relationships
The classic MongoDB modeling decision. A blog post with a handful of tags embeds well; a customer with thousands of orders does not.
Embedding vs. referencing a one-to-many relationship
// Embedded: small, bounded child list
{ title: "MongoDB Basics", tags: ["database", "nosql", "tutorial"] }
// Referenced: large, unbounded child list
// orders collection, referencing customer
{ _id: ObjectId("o1"), customerId: ObjectId("c1"), amount: 59.99 }đĸâī¸đĸ 7. Many-to-Many Relationships
Typically modeled with arrays of references on one or both sides, depending on which direction is queried more often.
Many-to-many via arrays of references
// students collection
{ _id: ObjectId("s1"), name: "Alice", courseIds: [ObjectId("c1"), ObjectId("c2")] }
// courses collection
{ _id: ObjectId("c1"), title: "Intro to MongoDB", studentIds: [ObjectId("s1")] }Caution
âģī¸ 8. Self-Referencing Relationships
A document can reference another document in the same collection, commonly used for hierarchies like categories or organizational charts.
A self-referencing category hierarchy
// categories collection
{ _id: ObjectId("cat1"), name: "Electronics", parentId: null }
{ _id: ObjectId("cat2"), name: "Laptops", parentId: ObjectId("cat1") }đĒ 9. Parent-Child Relationships
A common self-referencing pattern where each document stores a pointer to its parent, forming a tree structure such as an employee reporting hierarchy.
Modeling a management hierarchy
// employees collection
{ _id: ObjectId("e1"), name: "Priya", title: "CTO", managerId: null }
{ _id: ObjectId("e2"), name: "Arjun", title: "Engineer", managerId: ObjectId("e1") }đ 10. Data Duplication
Referenced relationships sometimes benefit from duplicating a small, rarely changing piece of related data to avoid an extra lookup on every read.
Duplicating a field to avoid a join
{
_id: ObjectId("o1"),
customerId: ObjectId("c1"),
customerName: "Alice", // duplicated for fast display without a $lookup
amount: 59.99
}Warning
đ 11. Document Linking
The simplest linking mechanism is storing the related document's _id in a field, then querying for it directly when needed.
Resolving a manual reference with a second query
const customer = await customers.findOne({ _id: order.customerId });âī¸ 12. Manual References
A manual reference is simply an _id value stored in another document â no special BSON type required. This is the recommended approach for the vast majority of relationships.
A manual reference
{
_id: ObjectId("o1"),
customerId: ObjectId("c1") // plain manual reference
}Note
đ 13. DBRefs
DBRefs are a legacy convention that embeds the collection name alongside the referenced _id, mainly useful when a field might reference documents from multiple collections.
A DBRef pointing to another collection
{
_id: ObjectId("comment1"),
attachedTo: { $ref: "posts", $id: ObjectId("p1") }
}Caution
đ 14. $lookup
The $lookup aggregation stage performs a left outer join against another collection in the same database, resolving references into embedded arrays.
Joining orders with their customer
await orders.aggregate([
{
$lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer"
}
},
{ $unwind: "$customer" }
]).toArray();Tip
đŗ 15. $graphLookup
Performs a recursive search through a collection, following a chain of references â perfect for hierarchies like organizational charts or category trees.
Finding an employee's full management chain
await employees.aggregate([
{ $match: { name: "Arjun" } },
{
$graphLookup: {
from: "employees",
startWith: "$managerId",
connectFromField: "managerId",
connectToField: "_id",
as: "managementChain"
}
}
]).toArray();Example
đ§ 16. Join Strategies
| Strategy | Best for |
|---|---|
| Embedding | Small, bounded, always-together data |
| Manual reference + $lookup | Larger or independently queried data |
| $graphLookup | Recursive hierarchies of unknown depth |
| Application-level joins | Cross-database or cross-service data |
đĒ 17. Nested Relationships
Relationships can be nested several levels deep â an order references a customer, who references a loyalty tier, and so on. Each level adds a $lookup stage.
Chaining multiple $lookup stages
await orders.aggregate([
{ $lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "customer" } },
{ $unwind: "$customer" },
{ $lookup: { from: "loyaltyTiers", localField: "customer.tierId", foreignField: "_id", as: "customer.tier" } }
]).toArray();Caution
đ 18. Recursive Relationships
Use $graphLookup's maxDepth option to bound how far a recursive relationship traversal goes, preventing runaway queries on deep or cyclic hierarchies.
Limiting recursion depth
{
$graphLookup: {
from: "categories",
startWith: "$parentId",
connectFromField: "parentId",
connectToField: "_id",
as: "ancestors",
maxDepth: 5
}
}⥠19. Relationship Performance
- Index the localField and foreignField used in every $lookup.
- Prefer embedding over $lookup when the joined data is small and read-heavy.
- Use the pipeline form of $lookup with an early $match to reduce the joined dataset.
- Bound $graphLookup traversals with maxDepth to avoid unpredictable query times.
đ 20. Data Consistency
Since MongoDB has no foreign key enforcement, keeping references valid â and duplicated fields in sync â is the application's responsibility.
Important
đī¸ 21. Relationship Design Patterns
Real applications typically combine several relationship styles. An order might embed its line items, reference its customer, and denormalize the customer's name for display.
Combining relationship styles in one document
{
_id: ObjectId("o1"),
customerId: ObjectId("c1"), // reference
customerName: "Alice", // denormalized
items: [ // embedded
{ productId: ObjectId("p1"), name: "Mouse", qty: 2 }
]
}đ 22. Best Practices
- Default to embedding for small, bounded, always-together data.
- Use manual references plus $lookup for larger or independently queried data.
- Reserve $graphLookup for genuinely recursive hierarchies, not simple parent-child links.
- Index every field used as a join key in $lookup or $graphLookup.
- Use transactions when duplicated or referenced data must change atomically together.
â ī¸ 23. Common Mistakes
- Modeling every relationship with $lookup, recreating relational-style joins unnecessarily.
- Embedding a one-to-many relationship that grows unbounded, risking the 16MB document limit.
- Forgetting to index localField/foreignField pairs used in frequent joins.
- Using $graphLookup without a maxDepth on data that may contain cycles.
- Letting denormalized duplicate fields drift out of sync with their source of truth.
Best Practice
â 24. Frequently Asked Questions
No. MongoDB does not enforce referential integrity â the application is responsible for keeping references valid.
Use $graphLookup when the relationship is recursive, like a category tree or reporting chain, and the depth isn't known in advance. A single-level relationship only needs $lookup.
Generally, no. Manual references are simpler and better supported by modern tooling; DBRefs are mostly a legacy convention.