MongoDB Relationships

In any real-world application, data is rarely isolated. A customer places orders. An order contains products. A product belongs to a category. These connections between data entities are called relationships. MongoDB handles relationships differently from SQL databases — instead of joining tables through foreign keys automatically, MongoDB gives control over how related data is stored and retrieved.

There are three main types of data relationships: one-to-one, one-to-many, and many-to-many. Each can be implemented using embedding or referencing in MongoDB.

One-to-One Relationship

A one-to-one relationship means one entity is associated with exactly one other entity — and vice versa. For example, one employee has one employee ID card. One user has one profile.

Implementation: Embedding (Recommended)

Since the data is tightly related and always accessed together, embedding the sub-document is the most efficient approach.

db.employees.insertOne({
  name: "Kavya Menon",
  designation: "Software Engineer",
  idCard: {
    cardNumber: "EMP-2025-001",
    issuedDate: ISODate("2023-06-01"),
    expiryDate: ISODate("2028-06-01")
  }
})

Fetching the employee also returns the ID card details in one single query.

Retrieving One-to-One Data

db.employees.findOne({ name: "Kavya Menon" })

The full document — including embedded ID card — returns in one call with no join required.

One-to-Many Relationship

A one-to-many relationship means one entity is associated with multiple others. A customer places many orders. A blog post has many comments. A department has many employees.

The implementation choice depends on how many "many" records exist and whether they are needed alongside the parent every time.

Option A: Embedding (One-to-Few)

When the number of related items is small and predictable, embedding works well.

db.blogPosts.insertOne({
  title: "Getting Started with MongoDB",
  author: "Rohit Sharma",
  comments: [
    { user: "Anil", text: "Great article!", date: ISODate("2024-12-01") },
    { user: "Sunita", text: "Very helpful, thanks.", date: ISODate("2024-12-02") }
  ]
})

Comments are embedded because they are always read together with the post and a post typically has a manageable number of them.

Option B: Referencing (One-to-Many at Scale)

When there are potentially thousands of related documents, referencing avoids document size problems.

// customers collection
db.customers.insertOne({
  _id: ObjectId("c001"),
  name: "Divya Iyer",
  email: "divya@example.com"
})

// orders collection — each order references its customer
db.orders.insertMany([
  { orderId: 1001, customerId: ObjectId("c001"), product: "Laptop", amount: 55000 },
  { orderId: 1002, customerId: ObjectId("c001"), product: "Mouse", amount: 850 },
  { orderId: 1003, customerId: ObjectId("c001"), product: "Keyboard", amount: 1200 }
])

The customer document stays lean. Each order holds the customer's ID to establish the relationship.

Querying Referenced One-to-Many Data

To find all orders for a specific customer:

db.orders.find({ customerId: ObjectId("c001") })

To combine customer and order data using the aggregation pipeline:

db.customers.aggregate([
  { $match: { _id: ObjectId("c001") } },
  {
    $lookup: {
      from: "orders",
      localField: "_id",
      foreignField: "customerId",
      as: "orderHistory"
    }
  }
])

Result:

{
  "_id": ObjectId("c001"),
  "name": "Divya Iyer",
  "email": "divya@example.com",
  "orderHistory": [
    { "orderId": 1001, "product": "Laptop", "amount": 55000 },
    { "orderId": 1002, "product": "Mouse", "amount": 850 },
    { "orderId": 1003, "product": "Keyboard", "amount": 1200 }
  ]
}

Many-to-Many Relationship

A many-to-many relationship means one entity can be associated with many others, and those others can also be associated with many entities. A student can enroll in many courses, and a course can have many students enrolled.

Implementation: Array of References (Recommended)

// students collection
db.students.insertMany([
  { _id: ObjectId("s001"), name: "Harshita Singh", enrolledCourseIds: [ObjectId("co001"), ObjectId("co002")] },
  { _id: ObjectId("s002"), name: "Tanmay Ghosh", enrolledCourseIds: [ObjectId("co001"), ObjectId("co003")] }
])

// courses collection
db.courses.insertMany([
  { _id: ObjectId("co001"), courseName: "Web Development", instructor: "Ms. Leela" },
  { _id: ObjectId("co002"), courseName: "Data Science", instructor: "Mr. Arora" },
  { _id: ObjectId("co003"), courseName: "Cloud Computing", instructor: "Dr. Singh" }
])

Each student stores an array of course IDs. No course data is duplicated — if a course name changes, it is updated in one place only.

Finding All Courses for a Student

db.students.aggregate([
  { $match: { name: "Harshita Singh" } },
  {
    $lookup: {
      from: "courses",
      localField: "enrolledCourseIds",
      foreignField: "_id",
      as: "courses"
    }
  },
  { $project: { name: 1, courses: 1, _id: 0 } }
])

Result:

{
  "name": "Harshita Singh",
  "courses": [
    { "courseName": "Web Development", "instructor": "Ms. Leela" },
    { "courseName": "Data Science", "instructor": "Mr. Arora" }
  ]
}

Finding All Students Enrolled in a Course

db.students.find({ enrolledCourseIds: ObjectId("co001") }, { name: 1, _id: 0 })

MongoDB checks if ObjectId("co001") exists inside the enrolledCourseIds array and returns all matching students.

Self-Referencing Relationships

Some data points back to the same collection. For example, an employee can have a manager who is also an employee in the same collection.

db.staff.insertMany([
  { _id: ObjectId("e001"), name: "Rajan Bhat", role: "Manager", managerId: null },
  { _id: ObjectId("e002"), name: "Sonal Kapoor", role: "Developer", managerId: ObjectId("e001") },
  { _id: ObjectId("e003"), name: "Vikas Jain", role: "Designer", managerId: ObjectId("e001") }
])

To find all team members reporting to Rajan Bhat:

db.staff.find({ managerId: ObjectId("e001") })

Relationship Design — Decision Table

RelationshipRelated ItemsApproachReason
One-to-OneAlways with parentEmbedSingle read, tight coupling
One-to-FewSmall, bounded listEmbedAll data in one document
One-to-ManyLarge or unboundedReferenceAvoid document size bloat
Many-to-ManyBoth sides largeReference with ID arraysAvoid data duplication
Self-referencingSame collectionReference (ID field)Parent-child within one collection

Avoiding Common Relationship Mistakes

Mistake 1 — Embedding Unbounded Arrays

Embedding an array that grows without limit (like all messages in a chat group inside the group document) will eventually exceed MongoDB's 16 MB document size limit. Reference instead.

Mistake 2 — Referencing Everything (Over-normalizing)

Referencing every piece of data, even tightly coupled data like an order's shipping address, forces multiple queries on every page load. Embed data that belongs together and rarely changes.

Mistake 3 — Duplicating Data Without a Plan

Sometimes duplicating data (denormalization) is intentional and beneficial in MongoDB. For example, storing the customer's name inside an order document avoids a lookup for displaying order history. But planned duplication requires updating all copies when the original changes — the application must handle this consistency manually.

Summary

MongoDB supports one-to-one, one-to-many, and many-to-many relationships through embedding and referencing. Embedding keeps related data in one document for fast reads. Referencing separates data into linked collections for large or shared datasets. The $lookup stage in aggregation pipelines performs joins between referenced collections. Self-referencing documents handle hierarchical relationships within a single collection. Choosing the right relationship model based on data access patterns is key to building efficient and maintainable MongoDB applications.

Leave a Comment