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

Sponsored

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 _id in 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 user document contains an address subdocument.
  • References — The parent stores the _id of the related document in another collection. Example: a user document contains an address_id field pointing to an address document.

💡 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 $lookup to 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 $unwind after $lookup for 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 $lookup performance.
  • Using $lookup without $unwind and 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

  1. 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.
  2. Two-way references — store the _id in both documents (e.g., user has profile_id, profile has user_id) — useful when accessing from both sides, but keep them in sync.
  3. Using a hybrid approach — embed the most frequently accessed fields, and reference the less common ones (e.g., embed avatarUrl but reference a large avatarData document).

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.
  • $lookup is the join mechanism for references, but it returns an array — always use $unwind for one-to-one.
  • Document size limit (16 MB) is a primary driver when deciding between embedding and referencing.
  • Index foreign keys to keep $lookup fast.
  • Consistency is simpler with embedding (single-document atomicity), while references require multi-document transactions for atomic updates.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.