Schema Design and Modeling

Embedding vs referencing, the core MongoDB modeling decision, and schema validation rules.

The core modeling decision: embed or reference

Relational design mostly asks "how do I split this into normalized tables?" (covered in the database-design section of this track). MongoDB design mostly asks a different question up front: for any given relationship, should the related data live embedded inside the same document, or referenced in a separate collection by id? Every schema decision in MongoDB ultimately comes back to this one trade-off.

Embedding

Embedding nests related data directly inside the parent document, as a sub-object or array. Take a blog post and its comments:

Javascript
// Embedded: comments live inside the post document
{
  _id: ObjectId("..."),
  title: "Getting started with MongoDB",
  author: "Liam Chen",
  body: "MongoDB is a document database...",
  comments: [
    { user: "Priya Nair", text: "Great intro!", postedAt: ISODate("2026-02-01") },
    { user: "Amara Diallo", text: "Very helpful, thanks.", postedAt: ISODate("2026-02-02") }
  ]
}

Reading a post and all of its comments together is now a single findOne — no join, no second query. That's the entire appeal of embedding: data that's almost always read together should usually live together.

Embedding fits when:

  • The embedded data has a clear parent and isn't independently queried or reused elsewhere (a comment doesn't exist meaningfully outside its post).
  • The embedded array has a bounded, reasonably small size (dozens, not tens of thousands).
  • The whole group is naturally read and displayed together (a post with its comments, an order with its line items).

Referencing

Referencing stores a related document in its own collection and links to it by id, the same conceptual shape as a relational foreign key:

Javascript
// posts collection
{ _id: ObjectId("post1"), title: "Getting started with MongoDB", author: "Liam Chen" }

// comments collection — referenced by postId
{ _id: ObjectId("c1"), postId: ObjectId("post1"), user: "Priya Nair", text: "Great intro!" }
{ _id: ObjectId("c2"), postId: ObjectId("post1"), user: "Amara Diallo", text: "Very helpful, thanks." }

Fetching a post's comments now needs a second query (or a $lookup in an aggregation pipeline, covered on the next page) — more work per read, but each comment is now an independent document that can be queried, updated, paginated, or deleted on its own without touching the parent post at all.

Referencing fits when:

  • The related data is large or unbounded (a wildly popular post could accumulate tens of thousands of comments — embedding all of them in one document risks hitting MongoDB's 16MB per-document size limit, and makes the parent document progressively slower to load regardless).
  • The related data needs to be queried independently of its parent ("show me all of this user's comments across every post," not just "show me this post's comments").
  • The same related data is shared across multiple parents (a product referenced from many order documents, rather than duplicating the full product details into every order).

Deciding for the blog example

A realistic answer for a blog: embed comments if the post/comment volume is modest and comments are always viewed alongside their post (a small blog, most posts with a handful of comments). Reference comments once volume grows large or comments need independent querying (a high-traffic platform where a single popular post could have thousands of comments, or where there's a "view all your comments across the site" page). Many real systems start embedded for simplicity and migrate to referencing once a specific collection's documents start approaching the size or growth pattern where embedding breaks down.

Embed Reference
Read cost One query for everything Extra query (or $lookup) per read
Write cost Whole document rewritten on update Independent, smaller writes
Document size limit Bounded by 16MB per document Not a concern — each document stays small
Independent querying of the child Awkward (requires unwinding arrays) Natural — it's its own collection
Data duplication across parents None (data lives in one place) None if referenced correctly (unlike copy-pasting the same data into many documents)

A hybrid approach: embed a summary, reference the rest

A common middle ground: embed just enough denormalized data for the common read path, and reference the rest for anything less frequent:

Javascript
{
  _id: ObjectId("post1"),
  title: "Getting started with MongoDB",
  author: "Liam Chen",
  commentCount: 128,          // denormalized summary, kept for cheap display
  recentComments: [ /* last 3 comments, embedded for the post preview */ ],
  // full comment history lives in a separate "comments" collection, referenced by postId
}

This trades a small amount of duplicated/denormalized data (which must be kept in sync deliberately, e.g. incrementing commentCount whenever a comment is inserted) for a fast common-case read, while keeping the full, unbounded comment history in its own collection.

Schema validation

MongoDB's schema is flexible by default, but a collection can still enforce structural rules with a validator — useful once an application's shape has stabilized and you want the database itself to reject malformed documents, similar in spirit to a relational NOT NULL/CHECK constraint:

Javascript
db.createCollection("products", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name", "price"],
      properties: {
        name: { bsonType: "string" },
        price: { bsonType: "double", minimum: 0 },
        stock: { bsonType: "int", minimum: 0 }
      }
    }
  }
});

With this validator in place, db.products.insertOne({ name: "Bad Product", price: -5 }) is rejected outright rather than silently stored. Validation can be added to an existing collection too, and set to warn (log but allow) rather than error (reject) during a migration period.

Common mistakes

  • Embedding an unbounded array (comments, log entries, activity history) that grows without limit — eventually risking the 16MB document size limit and, well before that, making every read and write against the parent document progressively slower.
  • Referencing everything by default out of relational habit, turning every simple read into a $lookup and giving up the document model's main benefit.
  • Duplicating data across many documents (copying a full product object into every order line item) without a deliberate plan for keeping duplicates in sync when the source changes — this is fine for immutable snapshots (an order should preserve the price at the time of purchase, even if the product's price changes later) but a bug magnet for anything expected to stay current.
  • Treating "schemaless" as "no validation ever" instead of adding $jsonSchema validation once a collection's shape is well understood.