Ch. 12 · MongoDB

MongoDB Embedding Versus Referencing

How to decide whether related MongoDB data goes inside one document or in its own collection, based on access patterns, growth and ownership.

~6 min readbeginnerupdated Oct 7, 2026

In a relational database the schema question is mostly settled before you look at queries: one table per entity, foreign keys between them, normalise. MongoDB hands you a real choice. A blog post’s comments can live inside the post document as an array, or in a comments collection with a postId field. Both work. Interviewers ask about this to see whether you choose based on how the data is used or just copy the relational habit.

The examples use mongosh syntax for MongoDB 7 and 8. They’re illustrative; no server was available while writing.

Two shapes for the same data

An online shop has customers with shipping addresses. Embedded:

{
  _id: ObjectId("..."),
  name: "Ana Lopes",
  email: "ana@example.com",
  addresses: [
    { label: "home", line1: "Rua Augusta 10", city: "Lisbon", postcode: "1100-053" },
    { label: "work", line1: "Av. da Liberdade 200", city: "Lisbon", postcode: "1250-147" }
  ]
}
JavaScript

Referenced:

// customers
{ _id: ObjectId("c1"), name: "Ana Lopes", email: "ana@example.com" }
// addresses
{ _id: ObjectId("a1"), customerId: ObjectId("c1"), label: "home", line1: "Rua Augusta 10", city: "Lisbon" }
{ _id: ObjectId("a2"), customerId: ObjectId("c1"), label: "work", line1: "Av. da Liberdade 200", city: "Lisbon" }
JavaScript

For addresses, embedding is the clear winner. The checkout page needs the customer and their addresses together, so it’s one read instead of two. A customer has a handful of addresses, not thousands. Addresses belong to that customer and are never shared. And updating “the customer and their default address” is a single atomic write.

The questions that decide it

Run through these four questions for each relationship.

1. Are they read together? If nearly every screen that shows the parent also shows the children, embedding saves a round trip or a $lookup. If the children are usually read on their own (an admin page listing all addresses in a postcode), separate documents are easier to query and index.

2. Is the child set bounded? This is the one people forget. A document can’t exceed 16 MB, and long before that, large documents hurt: every read pulls the whole thing over the network, and every push to an array rewrites a growing document. A product with five images is bounded. A product with every customer review ever written is not. A popular product can collect 80,000 reviews; that array will eventually hit the limit and makes every product page read slow long before then.

3. Who owns the child? If the child only exists as part of the parent (an order’s line items), embed. If it’s shared by many parents (a product referenced by thousands of orders, an author shared by many books), reference it. Embedding shared data means copying it everywhere, and then you need to decide what happens when it changes.

4. Does it change independently? Data that’s updated on its own schedule, by different code, is usually better in its own collection. A user’s profile and their login sessions change at very different rates; sessions get their own collection, probably with a TTL index.

Duplication is allowed, with intent

Relational training says duplication is bad. In MongoDB, controlled duplication is a common and good design. The order line item is the textbook case:

{
  _id: 50071,
  customerId: ObjectId("c1"),
  createdAt: ISODate("2026-09-14T10:02:00Z"),
  items: [
    { productId: ObjectId("p9"), name: "Trail Runner 3", price: 89.0, qty: 1 }
  ]
}
JavaScript

The order stores the product name and price at the time of purchase. That’s not a cache that might go stale; it’s the historically correct value. If the product is renamed or repriced next month, the order must still show what the customer actually paid. Interviewers like this example because duplication here isn’t a compromise, it’s the requirement.

Where duplicated data does need to stay in sync (say you copy an author’s display name onto each of their posts to avoid a lookup), be clear about the cost: renaming the author means an updateMany across their posts, and for a moment some posts show the old name. That’s acceptable if names change rarely and the slight lag doesn’t matter. Say so explicitly in an interview; it shows you see the trade-off rather than hoping nobody asks.

Patterns between the extremes

You’re not stuck choosing all or nothing.

  • Subset pattern. Embed the 10 most recent reviews on the product for the product page, and keep all reviews in a reviews collection. The page loads with one read; the “see all reviews” page queries the collection with pagination.
  • Extended reference. Store the referenced document’s id plus the two or three fields you always display ({ authorId, authorName }), and fetch the rest only when needed.
  • Bucketing. For sensor readings or chat messages, group children into documents of, say, 200 per bucket. You get fewer, bounded documents instead of one unbounded array or millions of tiny documents. (Time-series collections do this for you for measurement data.)

Common modelling mistakes

Unbounded arrays. The followers array on a user, the events array on a device. They work in a demo and degrade quietly in production. If you can’t name a sensible upper bound, use a separate collection.

Copying the ER diagram. Making a collection for every table you’d have in Postgres, then joining everything with $lookup, gives you relational modelling without relational join performance. Start from the screens and API calls, and shape documents to answer them.

Embedding many-to-many both ways. Storing every student’s course list inside the student and every course’s student list inside the course doubles every write and lets the two sides drift. Pick one side to hold references, usually the smaller and more bounded one, or use a separate enrollment collection when the relationship has its own data (grade, enrollment date).

Interview practice

You’re designing a project-management tool. A project has tasks; tasks have comments. Typical projects have 50 to 500 tasks; some tasks get hundreds of comments. The main screen shows a project’s task list with titles, assignees and status. Opening a task shows its full description and all comments. How do you model it?

Answer

I’d look at access patterns first. The project screen needs task summaries for one project, and the task screen needs one task plus its comments.

Tasks should be their own collection with a projectId field and an index on { projectId: 1, status: 1 } or similar. Embedding up to 500 tasks, each with a full description, in the project document makes the project document large and turns every task edit into a write on a big shared document, which also means two people editing different tasks contend on the same document. Tasks also change independently.

Comments should be referenced too, in a comments collection keyed by taskId, because “hundreds of comments” on some tasks is unbounded in practice and comments are appended constantly. To make the task screen fast, I might embed a small commentCount and the latest two or three comments on the task (subset pattern), updated with $inc and $push with $slice: -3 when a comment is added.

On the project screen, I’d avoid a $lookup by querying tasks directly with a projection that returns only title, assignee and status. If assignee names are shown, I’d store { assigneeId, assigneeName } on the task, accepting that renaming a user requires an updateMany. Each choice ties back to a bound, an access pattern or an ownership rule, which is what the interviewer is checking.

Further reading

More in MongoDB

read ✓MongoDB · easy

MongoDB Missing Fields Versus Null

Why { field: null } in MongoDB also matches documents without the field, and how $exists and $type let you tell missing and null apart.

~5 min readread →
read ✓MongoDB · mid

MongoDB Transactions and Data Modeling

When a MongoDB multi-document transaction is the right tool, what it costs, how to retry it correctly, and when a better model removes the need.

~5 min readread →
read ✓MongoDB · mid

MongoDB Bulk Writes

Batch a nightly upsert job with bulkWrite, pick ordered or unordered mode, and read writeErrors so partial failures don't get lost or retried.

~6 min readread →
esc