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?
- 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.
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.
// 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 }
);- 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.
A student-course enrollment stores progress and enrollment status. Which model is most appropriate?