Embedded Documents and References

Model related data in MongoDB: embed documents for data that belongs together, reference other documents by _id when data is shared or unbounded, query and update nested fields, and understand the trade-offs of duplicating data.

Lesson 10 of 43Intermediate17 min read

In a relational database, related data is split into tables and joined back together when you query it. MongoDB gives you a choice. You can embed related data inside a document, so a customer's address lives in the customer document and an order's items live in the order. Or you can reference it, storing another document's _id, like a foreign key, so a review points at the product it belongs to.

Embedding makes reads simple and fast: one query returns everything needed for a page, and updates to one document are atomic, so an order and its items always change together. Referencing avoids duplicating data that's shared by many documents, and keeps documents small when related data would grow without limit.

Choosing between them is the heart of MongoDB data modelling. In this lesson you'll embed one-to-one and one-to-few data, query and update nested fields, reference data in one-to-many and many-to-many relationships, use the extended reference pattern that copies a few useful fields, and learn a practical checklist for deciding.

Embedding

MongoDB Shell
{
  _id: 1,
  name: 'Ada Lovelace',
  address: { street: '12 Bridge St', city: 'Leeds', postcode: 'LS1 4AP' },
  phones: [
    { type: 'home', number: '0113 496 0000' },
    { type: 'mobile', number: '07700 900123' }
  ]
}
  • Embed when the data belongs to the parent, is usually read with it, and is bounded in size: an address, a few phone numbers, an order's items.
  • Query nested fields with dot notation: { 'address.city': 'Leeds' }, { 'phones.type': 'mobile' }.
  • Update one nested field with $set: { 'address.city': 'York' } so the rest of the address stays.
  • Writes to a single document are atomic, which often removes the need for transactions.

Referencing

MongoDB Shell
// products
{ _id: 'p1', name: 'Desk lamp', price: 24 }
// reviews, each pointing at a product
{ _id: 'r1', productId: 'p1', rating: 5, text: 'Bright and sturdy' }
  • Reference when the related data is large or unbounded (thousands of reviews), shared by many documents (a category used by many products), or changes independently and often.
  • Many-to-many relationships use arrays of ids: a student document with courseIds: ['c1', 'c2'].
  • Read referenced data with a second query, or join it in the database with $lookup.
  • Index reference fields such as productId, because you'll query by them constantly.

The extended reference pattern

A middle ground: store the reference and a copy of the few fields you display most:

MongoDB Shell
{ _id: 'o1', customer: { _id: 'c7', name: 'Ada Lovelace' }, total: 59.9 }

The order list can show the customer's name without another query, and the full customer is still one lookup away. The cost is keeping copies in sync, so copy only fields that rarely change, or that should stay as they were at the time, such as the delivery address and prices on a past order.

MongoDB Data Modelling Syntax

Modelling patterns

// embedded one-to-one and one-to-few
{ _id: 1, address: { city: 'York' }, phones: [{ type: 'mobile', number: '...' }] }
db.users.find({ 'address.city': 'York' })
db.users.updateOne({ _id: 1 }, { $set: { 'address.city': 'Leeds' } })
// one-to-many by reference
{ _id: 'r1', productId: 'p1', rating: 4 }
db.reviews.createIndex({ productId: 1 })
db.reviews.find({ productId: 'p1' })
// many-to-many
{ _id: 's1', courseIds: ['c1', 'c2'] }
db.courses.find({ _id: { $in: db.students.findOne({ _id: 's1' }).courseIds } })

MongoDB Data Modelling Summary

  • Embedding stores related data inside a document; referencing stores another document's _id.
  • Embed data that's read together, bounded and owned by the parent; writes to it are atomic.
  • Reference data that's large, unbounded, shared or independently changing; index reference fields.
  • Dot notation queries and updates nested fields without replacing whole sub-documents.
  • The extended reference pattern copies a few display fields alongside the id.

MongoDB Data Modelling Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Embedded address and phones

MongoDB Shell
db.customers.insertMany([
  { _id: 1, name: 'Ada', address: { city: 'Leeds', postcode: 'LS1 4AP' }, phones: [{ type: 'mobile', number: '07700 900123' }] },
  { _id: 2, name: 'Ben', address: { city: 'York', postcode: 'YO1 7HH' }, phones: [{ type: 'home', number: '01904 000000' }] }
])
db.customers.find({ 'address.city': 'Leeds' }, { name: 1, _id: 0 })
db.customers.find({ 'phones.type': 'home' }, { name: 1, _id: 0 })
db.customers.updateOne({ _id: 2 }, { $set: { 'address.city': 'Harrogate' } })
db.customers.findOne({ _id: 2 }, { address: 1, _id: 0 })
Output
{ acknowledged: true, insertedIds: { '0': 1, '1': 2 } }
[ { name: 'Ada' } ]
[ { name: 'Ben' } ]
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
{ address: { city: 'Harrogate', postcode: 'YO1 7HH' } }
Line-by-line explanation
  1. Lines 2 to 3Each customer's address and phones live inside their document.
  2. Line 5Dot notation queries a field inside the embedded address.
  3. Line 6It also reaches into arrays of embedded documents.
  4. Line 7Only the city changes; the postcode is kept.
Example 2

Products and reviews by reference

MongoDB Shell
db.products.insertMany([
  { _id: 'p1', name: 'Desk lamp' },
  { _id: 'p2', name: 'Mug' }
])
db.reviews.insertMany([
  { productId: 'p1', rating: 5, text: 'Bright' },
  { productId: 'p1', rating: 3, text: 'Wobbly base' },
  { productId: 'p2', rating: 4, text: 'Holds a lot' }
])
db.reviews.createIndex({ productId: 1 })

const product = db.products.findOne({ name: 'Desk lamp' })
const reviews = db.reviews.find({ productId: product._id }, { _id: 0, rating: 1, text: 1 }).toArray()
print(product.name, reviews.length, 'reviews')
reviews
Output
{ acknowledged: true, insertedIds: { '0': 'p1', '1': 'p2' } }
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('...'),
    '1': ObjectId('...'),
    '2': ObjectId('...')
  }
}
productId_1
Desk lamp 2 reviews
[ { rating: 5, text: 'Bright' }, { rating: 3, text: 'Wobbly base' } ]
Line-by-line explanation
  1. Lines 6 to 8Each review stores the _id of its product instead of being embedded.
  2. Line 10An index on the reference field makes "reviews for this product" fast.
  3. Lines 12 to 13Two queries: first the product, then its reviews by id.
  4. Line 15Reviews can grow to any number without making the product document bigger.
Example 3

An extended reference on orders

MongoDB Shell
db.customers.insertOne({ _id: 'c7', name: 'Ada Lovelace', email: 'ada@example.com', tier: 'gold' })
db.orders.insertMany([
  { _id: 'o1', customer: { _id: 'c7', name: 'Ada Lovelace' }, total: 59.9 },
  { _id: 'o2', customer: { _id: 'c7', name: 'Ada Lovelace' }, total: 12.5 }
])
db.orders.find({}, { _id: 1, 'customer.name': 1, total: 1 })

db.customers.updateOne({ _id: 'c7' }, { $set: { name: 'Ada King' } })
db.orders.updateMany({ 'customer._id': 'c7' }, { $set: { 'customer.name': 'Ada King' } }).modifiedCount
db.orders.distinct('customer.name')
Output
{ acknowledged: true, insertedId: 'c7' }
{ acknowledged: true, insertedIds: { '0': 'o1', '1': 'o2' } }
[
  { _id: 'o1', customer: { name: 'Ada Lovelace' }, total: 59.9 },
  { _id: 'o2', customer: { name: 'Ada Lovelace' }, total: 12.5 }
]
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
2
[ 'Ada King' ]
Line-by-line explanation
  1. Lines 3 to 4Each order stores the customer's id plus their name, the field the order list shows.
  2. Line 6Order lists need no second query to show the customer's name.
  3. Lines 8 to 9When the name changes, the copies must be updated too, the cost of duplication.

Common MongoDB Data Modelling Mistakes

Embedding unbounded data

Don't
{ _id: 'p1', name: 'Lamp', reviews: [ /* every review ever written, growing forever */ ] }
Do
{ _id: 'r1', productId: 'p1', rating: 5 }   // reviews in their own collection

Splitting data that's always read together

Don't
// users, user_addresses and user_preferences collections, three queries for every profile page
Do
{ _id: 1, name: 'Ada', address: { city: 'Leeds' }, preferences: { theme: 'dark' } }

Forgetting to index references

Don't
db.reviews.find({ productId: 'p1' })        // scans every review in the collection
Do
db.reviews.createIndex({ productId: 1 })
db.reviews.find({ productId: 'p1' })

MongoDB Data Modelling in Real Projects

Modelling in real systems

Online shops embed line items and the delivery address in each order, because an order should keep exactly what was bought and where it went, even if the product or address changes later. Product catalogues reference categories and brands that many products share. Social apps reference followers and posts, which can grow without limit, while profile settings are embedded in the user document.

MongoDB Data Modelling FAQ

When should I embed documents and when should I use references in MongoDB?

Embed data that belongs to the parent, is read with it and stays bounded, such as an order's items or a user's addresses. Use references for data that grows without limit, is shared by many documents or changes on its own schedule, such as reviews, authors or categories. The deciding question is usually how your most frequent page or API call reads the data.

How do I update a field inside an embedded document?

Use dot notation with $set: db.users.updateOne({ _id: 1 }, { $set: { 'address.city': 'York' } }). This changes only city and keeps the rest of the address. Writing { $set: { address: { city: 'York' } } } instead replaces the whole embedded document, losing the street and postcode.

How deeply can MongoDB documents be nested?

The documented limit is 100 levels of nesting, but you'll never want to get close. Two or three levels are typical, and deeper structures make queries, indexes and updates harder to write. If you find yourself nesting very deeply, some of that data probably belongs in its own collection.

Can I create an index on a field inside an embedded document?

Yes. Use dot notation in the index key: db.users.createIndex({ 'address.city': 1 }). Queries such as { 'address.city': 'Leeds' } then use the index. For arrays of embedded documents, such as phones.number, MongoDB creates a multikey index with one entry per array element.

Is a one-to-many relationship better embedded or referenced?

It depends on how many the "many" is. One-to-few, such as a person's phone numbers, is a natural fit for an embedded array. One-to-thousands or one-to-millions, such as a product's reviews or a sensor's readings, should be referenced, with the child documents storing the parent's id and an index on that field.

MongoDB Data Modelling Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the query to find customers in Leeds using the embedded address.

db.customers.find({ '': 'Leeds' })
Multiple choiceeasy

When is embedding usually the better choice?

Multiple choicemedium

Why might you store the customer's name inside each order as well as the customer id?

Predict the outputmedium

What remains in address after this update if it was { city: 'Leeds', postcode: 'LS1' }?

db.c.updateOne({ _id: 1 }, { $set: { 'address.city': 'York' } })
Multiple choicehard

Why can embedding reduce the need for transactions?

MongoDB Data Modelling Quiz

5 questions to check what you learned.

  1. What does a reference store?

    • A copy of the whole related document
    • A URL
    • An index
    • The _id of the related document
  2. Which relationship suits referencing?

    • A product's thousands of reviews
    • A user's single address
    • An order's three line items
    • A user's theme setting
  3. How do you model many-to-many in MongoDB?

    • It isn't possible
    • Embed everything twice
    • Store an array of ids on one or both sides
    • Use a single huge document
  4. What should you add for fields used as references?

    • A TTL
    • An index
    • A unique constraint always
    • Nothing
  5. Why do past orders often copy prices and addresses instead of referencing them?

    • To save space
    • References don't work with numbers
    • They must show what was true at the time of the order, even if the product or address changes later
    • For faster deletes