One-to-One, One-to-Many, and Many-to-Many Relationships

Model the relationship, not just the entities

One-to-one

If two pieces of information are almost always read together and share a lifecycle, embed one in the other. If the related data is sensitive, large, optional, independently permissioned, or accessed through a separate workflow, use a separate collection and a unique reference.

Example: a user may have an optional securitySettings document. If it is stored separately, enforce one settings document per user with a unique index.

db.securitySettings.createIndex({ userId: 1 }, { unique: true });

One-to-many

For a small bounded set, embed an array. For an unbounded set, store child documents separately with a parent reference. The “many” side often carries the reference because each child belongs to one parent.

Many-to-many

A course can have many students, and a student can enroll in many courses. A dedicated enrollment collection models the relationship and its own attributes:

// enrollments
{
  _id: ObjectId("65a000000000000000000040"),
  studentId: ObjectId("65a000000000000000000010"),
  courseId: ObjectId("65a000000000000000000041"),
  enrolledAt: ISODate("2026-03-01T09:00:00Z"),
  status: "active",
  progressPercent: 35
}

The relationship is a first-class entity because it contains enrolledAt, status, and progressPercent, not just two identifiers.

db.enrollments.createIndex(
  { studentId: 1, courseId: 1 },
  { unique: true }
);

db.enrollments.createIndex({ courseId: 1, status: 1 });

The compound unique index prevents duplicate enrollments for the same student-course pair. Its field order supports queries beginning with studentId; the second index supports course-based status filtering.

Avoid giant inverse arrays

It may seem convenient to store all course IDs in a student's document and all student IDs in a course document. That can duplicate relationship state and create unbounded arrays. If the relationship itself has attributes or can become large, a join collection is usually easier to manage.

Relationship lifecycle questions

For every relationship, decide:

  • Can the child exist without the parent?
  • Can a parent have a very large number of children?
  • Must duplicate relationships be prevented?
  • Does the relationship have its own fields?
  • What should happen on deletion or archival?
  • Which direction is queried most often?
Specifications & Requirements
  • One-to-one:One record corresponds to at most one related record.
  • One-to-many:One parent has multiple related children.
  • Many-to-many:Records on both sides can relate to many records on the other side.
Note

When a relationship has its own fields, lifecycle, or uniqueness rules, model it as a first-class collection rather than hiding it inside two potentially unbounded arrays.

Query enrollment history and course roster
// A student's enrollments
db.enrollments.find({ studentId })
  .sort({ enrolledAt: -1 });

// Active students in a course
db.enrollments.find({
  courseId,
  status: "active"
});

// The unique compound index prevents duplicate pairs
db.enrollments.createIndex(
  { studentId: 1, courseId: 1 },
  { unique: true }
);
Key Takeaways
  • Embed bounded relationships when they are read and changed together.
  • Use a child collection for large one-to-many relationships.
  • Use a relationship collection when many-to-many links have attributes or uniqueness requirements.
Knowledge Check

A student-course enrollment stores progress and enrollment status. Which model is most appropriate?