Model One-to-One Relationships
Learn to model one-to-one relationships in MongoDB documents — embed or reference? Step-by-step guide with hands-on exercise and troubleshooting.
Focus: model one-to-one relationships in documents
Have you ever stored a user’s profile and their address in two separate collections, only to wrestle with joins and wonder why a simple lookup takes three round trips? Modeling one-to-one relationships in MongoDB is a decision that affects performance, consistency, and how naturally your data maps to your application. Get this wrong, and you’ll either bloat your documents with redundant fields or create a tangled web of references. In this lesson, you’ll learn the two primary ways to model one-to-one relationships—embedding and referencing—and gain a clear mental model to choose the right one for your data.
The problem this lesson solves
Relational databases make one-to-one relationships seem trivial: add a foreign key and you’re done. MongoDB, however, doesn’t enforce joins. The $lookup aggregation stage exists, but every join costs performance and complexity. The real problem is schema design: where do you put the related data?
- Embed the related fields directly in the parent document.
- Reference the related document by storing its
_idin the parent.
Both approaches are valid, but they have very different trade-offs. Choosing the wrong one leads to:
- Fragmented reads — fetching related data requires extra queries.
- Data duplication — embedding the same data in multiple documents.
- Unnecessary updates — changing one field forces updates across many documents.
By the end of this lesson, you’ll be able to confidently design a data model that fits your access patterns, not just your relational habits.
Core concept / mental model
Think of a one-to-one relationship as a cardinality of 1:1 — one owner has exactly one address, one user has exactly one profile, one order has exactly one invoice. In MongoDB, you have two weapons in your schema design arsenal: embedded documents and references.
- Embedded documents — The related data is a subdocument inside the parent. Example: a
userdocument contains anaddresssubdocument. - References — The parent stores the
_idof the related document in another collection. Example: auserdocument contains anaddress_idfield pointing to anaddressdocument.
💡 Mental shortcut: Ask yourself — “Do I always access this data together?” If the answer is yes, embed. If you sometimes need the related data independently, reference.
Think of embedding like putting all the information on one index card. Everything is in one place — fast to read, but you can’t share that card between two files. Referencing is like having a business card with a phone number — you can call, but you have to make a call (a query) to get the details.
How it works step by step
Let’s walk through the decision process for a real-world scenario: modeling a user and their profile data.
Step 1: Identify the relationship
Confirm it’s truly one-to-one. Does each user have exactly one profile? Does each profile belong to exactly one user? If yes, proceed.
Step 2: Check your access patterns
- Do you always display the user and profile together? (e.g., a dashboard showing name + bio) → Embed.
- Do you sometimes need the profile without the user? (e.g., a separate analytics service that only reads profiles) → Reference.
Step 3: Consider document size limits
MongoDB documents have a 16 MB limit. If the related data is large (e.g., a photo as a binary string), embedding might push you over the limit. In that case, reference.
Step 4: Decide and implement
Embedding — Create the parent document with the related fields nested inside.
// Embedded one-to-one: user + profile
{
_id: ObjectId("64a1f5c2e4b0a1a2b3c4d5e6"),
username: "johndoe",
email: "john@example.com",
profile: {
fullName: "John Doe",
bio: "Software developer and coffee enthusiast.",
avatarUrl: "/images/john.jpg"
}
}
Referencing — Create two collections, and store the reference in one of them.
// users collection
{
_id: ObjectId("64a1f5c2e4b0a1a2b3c4d5e6"),
username: "johndoe",
email: "john@example.com",
profile_id: ObjectId("64a1f6d3e5b0a1a2b3c4d5e7")
}
// profiles collection
{
_id: ObjectId("64a1f6d3e5b0a1a2b3c4d5e7"),
fullName: "John Doe",
bio: "Software developer and coffee enthusiast.",
avatarUrl: "/images/john.jpg"
}
Step 5: Retrieve the data
- Embedded: One query returns everything.
- Referenced: You need
$lookupto join in one query, or two separate queries.
Hands-on walkthrough
Let’s put this into practice. We’ll use mongosh (the MongoDB shell) and a simple example of a customer and their membership details.
Scenario: Customer membership
Each customer has exactly one membership record (level, points, joined date). Our app always shows membership details on the customer dashboard, and we rarely query memberships independently.
1. Connect to MongoDB and switch to a test database
mongosh
use shop
2. Insert a customer with an embedded membership document
db.customers.insertOne({
_id: ObjectId(),
name: "Alice Smith",
email: "alice@example.com",
membership: {
level: "Gold",
points: 1024,
joined: ISODate("2023-01-15T00:00:00Z")
}
})
3. Query the customer and get membership automatically
db.customers.findOne({ email: "alice@example.com" })
Output:
{
_id: ObjectId("64a1f5c2e4b0a1a2b3c4d5e6"),
name: "Alice Smith",
email: "alice@example.com",
membership: {
level: "Gold",
points: 1024,
joined: ISODate("2023-01-15T00:00:00Z")
}
}
Notice how the embedded subdocument is returned automatically — no extra query needed.
4. Now, model the same scenario with a reference
We’ll create two collections: customers and memberships.
// Insert a membership and capture its _id
const membershipId = ObjectId()
db.memberships.insertOne({
_id: membershipId,
level: "Gold",
points: 1024,
joined: ISODate("2023-01-15T00:00:00Z")
})
// Insert a customer referencing that membership
const customerId = ObjectId()
db.customers.insertOne({
_id: customerId,
name: "Alice Smith",
email: "alice@example.com",
membership_id: membershipId
})
5. Query the referenced data with $lookup
db.customers.aggregate([
{
$lookup: {
from: "memberships",
localField: "membership_id",
foreignField: "_id",
as: "membership"
}
},
{ $unwind: "$membership" } // because $lookup returns an array
]).pretty()
Output:
{
_id: ObjectId("64a1f5c2e4b0a1a2b3c4d5e6"),
name: "Alice Smith",
email: "alice@example.com",
membership_id: ObjectId("64a1f6d3e5b0a1a2b3c4d5e7"),
membership: {
level: "Gold",
points: 1024,
joined: ISODate("2023-01-15T00:00:00Z")
}
}
💡 Pro tip: Always use
$unwindafter$lookupfor a one-to-one relationship to flatten the resulting array. Otherwise, you’ll get an array containing the single document.
Compare options / when to choose what
| Criteria | Embedding | Referencing |
|---|---|---|
| Read efficiency | Single query, fast | Requires $lookup or extra queries |
| Write efficiency | Update parent and child together in one operation | Must update multiple collections |
| Data consistency | Atomic updates within one document | No atomic multi-document updates (until MongoDB 4.0 transactions) |
| Document size | Can exceed 16 MB if child is large | Keeps documents small |
| Independent access | Cannot query child alone without parent | Can query child collection directly |
| Data duplication | None | References avoid duplication |
| Schema flexibility | Both are flexible, embedding is simpler | References require joining |
When to choose embedding
- Always access data together — e.g., user + profile on a dashboard.
- The child is small and unlikely to grow beyond the 16 MB limit.
- You want atomic updates — updating the parent and child in a single operation.
When to choose referencing
- You need to query the child independently — e.g., a separate service that only reads profiles.
- The child is large — e.g., a document containing a base64-encoded image.
- You need to share the child across multiple parents — but then it’s not strictly one-to-one; still, it’s a clue that referencing may be more flexible.
Troubleshooting & edge cases
1. You embed a large subdocument and hit the 16 MB limit
Symptom: BSONObj size: X exceeds max object size: 16777216
Fix: Convert the subdocument into a separate collection and reference it by _id.
2. You referenced a document but forgot to index the foreign key
Symptom: $lookup performs slowly on large collections.
Fix: Create an index on the foreign key field:
db.customers.createIndex({ membership_id: 1 })
3. You used $lookup and got an array instead of an object
Symptom: The result has an membership field that is an array, not a document.
Cause: $lookup always returns an array (even for one-to-one).
Fix: Apply $unwind to flatten the array.
4. You updated the child document but the parent has stale data
Symptom: After updating a profile, the user document still shows old data — but wait, that’s only a problem if you embedded the data in two places. In a true one-to-one embedding, there’s no duplication. If you referenced, you must update the referenced document, not the parent. Fix: ensure your update targets the correct collection.
What you learned & what's next
You’ve learned how to model one-to-one relationships in documents using embedding and referencing. You understand the trade-offs between them and when to choose each approach based on access patterns, document size, and data consistency. You also practiced both techniques with mongosh, including using $lookup to join referenced data.
Next up: You’ll dive into modeling one-to-many relationships — where a single document (like a blog post) relates to many documents (like comments). The same decision process applies, but the scale of the “many” side changes the calculus. You’ll learn when embedding becomes impractical and referencing becomes essential.
Keep this lesson handy as your schema design checklist — it will pay off every time you face a new relationship in MongoDB.
Practice recap
Now try this mini exercise: In your own MongoDB instance, create a user and a passport collection. First, embed the passport details (e.g., passport number, expiry date) as a subdocument in the user document. Then, create a second user that references a passport document by _id. Use $lookup to join them and verify you get the expected output. Experiment with changing one field and see how embedding vs referencing behaves.
Common mistakes
- Blindly embedding all one-to-one data without considering the 16 MB document size limit — larger subdocuments can push you over.
- Forgetting to add an index on the foreign key field when using references — this causes slow
$lookupperformance. - Using
$lookupwithout$unwindand getting an array instead of a single object — results in confusing data structures. - Updating the child collection when you embedded the data, or vice versa — always check your model first.
- Choosing embedding just because it's easier, even when you need to query the child data independently.
Variations
- Use a subdocument with a one-to-one embedded array (e.g.,
profile: [ { ... } ]) but that adds unnecessary complexity — stick to a single subdocument for true one-to-one. - Two-way references — store the
_idin both documents (e.g., user has profile_id, profile has user_id) — useful when accessing from both sides, but keep them in sync. - Using a hybrid approach — embed the most frequently accessed fields, and reference the less common ones (e.g., embed
avatarUrlbut reference a largeavatarDatadocument).
Real-world use cases
- User profile pages where the profile is always shown with the user — embed to avoid extra queries.
- Customer and their loyalty membership details — embed if always displayed together; reference if you need to query memberships independently.
- User and their sensitive authentication data (e.g., password hash) — reference to keep the user document small and secure.
Key takeaways
- One-to-one relationships in MongoDB are modeled either by embedding the child document inside the parent or by referencing it with a foreign key.
- Embed when you always access the parent and child together and the child is small; reference for independent access or large subdocuments.
$lookupis the join mechanism for references, but it returns an array — always use$unwindfor one-to-one.- Document size limit (16 MB) is a primary driver when deciding between embedding and referencing.
- Index foreign keys to keep
$lookupfast. - Consistency is simpler with embedding (single-document atomicity), while references require multi-document transactions for atomic updates.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.