Modeling relationships
Every relationship between entities falls into one of three shapes by cardinality — how many of one thing relate to how many of another. A user has one profile (one-to-one). A user writes many posts (one-to-many). A student enrolls in many courses, and each course has many students (many-to-many). In a relational database the shape dictates a fixed mechanic: foreign keys and join tables. In MongoDB the shape only narrows your options; you still choose embed or reference for each, guided by the trade-offs from the previous lesson. This lesson walks the three shapes and the size limits that constrain them.
One-to-one
Section titled “One-to-one”When one document relates to exactly one other, embedding is usually the natural fit — the related data is bounded, owned by a single parent, and read alongside it. A user and their profile become one document:
{ "_id": "user_42", "profile": { "displayName": "Ada Lovelace", "bio": "Writes algorithms for engines that do not exist yet.", "joinedYear": 2024 }}flowchart LR U["User document"] --> P["profile sub-object embedded inside"]
You would only split a one-to-one into two documents if the related part is large, rarely read, or accessed independently — for example, keeping a heavyweight binary or a rarely-touched audit block in its own collection so the hot document stays small.
One-to-many
Section titled “One-to-many”A one-to-many relationship — one parent, many children — has two good shapes. If the children are few and bounded, embed them as an array. A blog post with its handful of tags is a clean embed:
{ "_id": "post_310", "title": "Why documents win", "tags": ["mongodb", "modeling", "nosql"]}But when the “many” side can grow large or is queried on its own, reference from the child instead: each child document stores the parent’s id. A user with potentially thousands of posts keeps the posts in their own collection, each pointing back:
{ "_id": "post_310", "authorId": "user_42", "title": "Why documents win" }flowchart TB
subgraph FewBounded["Few and bounded — embed array"]
A0["Post document"] --> A1["tags: array of values"]
end
subgraph ManyUnbounded["Many or unbounded — reference from child"]
B0["User document"]
B1["Post: authorId points to user"]
B2["Post: authorId points to user"]
B1 --> B0
B2 --> B0
end The dividing question is “could this list grow without limit?” A post’s tags will not; a user’s posts might. That single question usually picks the model for you.
Many-to-many
Section titled “Many-to-many”In a many-to-many relationship, each side relates to many of the other — students and courses, articles and tags, actors and films. The clean approach is referencing with id arrays, on one side or both. Putting the array on the side you most often read from gives you the fastest common query. Here each student carries the courses they are enrolled in:
{ "_id": "student_7", "name": "Linus", "courseIds": ["c_db", "c_os", "c_net"] }And the course documents stand on their own, referenced by id:
{ "_id": "c_db", "title": "Database Systems", "term": "Fall 2026" }flowchart TB S1["Student: courseIds array"] S2["Student: courseIds array"] C1["Course c_db"] C2["Course c_os"] S1 --> C1 S1 --> C2 S2 --> C1
Storing the reference array on both sides (students list their courses and courses list their students) makes both directions fast to read, but doubles the write work — enrolling a student now means updating two documents and keeping them consistent. Choose two-sided references only when you genuinely read both directions often.
The 16 MB limit and unbounded arrays
Section titled “The 16 MB limit and unbounded arrays”Every MongoDB document has a hard ceiling of 16 megabytes. That is generous for normal data, but it turns embedding into a trap whenever the embedded array has no natural upper bound. An order with a few line items is safe forever; a chat room that embeds every message, or a stock ticker that embeds every price tick, will eventually slam into the limit — and long before it does, the ever-growing document becomes slow to read and rewrite, because the server moves the whole document on each update.
The rule that follows: never embed an array that can grow without bound. If the count is open-ended, reference the children into their own collection, or use the Bucket pattern from the next lesson to cap each document’s growth. Embed only what is naturally limited.
Tips and gotchas
Section titled “Tips and gotchas”- The cardinality shape narrows your options, but you still pick embed or reference for each relationship based on size, growth, and access — there is no automatic answer.
- One-to-one almost always embeds; the exception is a large or rarely-read sub-part that earns its own collection.
- For one-to-many, ask “can the many side grow without limit?” If yes, reference from the child. If no, an embedded array is simpler and faster.
- Two-sided references in many-to-many make both reads fast but every write must update both sides — only worth it when both directions are hot.
- The 16 MB limit is a hard wall, but document performance degrades well before you reach it. Treat unbounded growth as the real enemy, not just the ceiling.