MongoDB Schema Design: Embed or Reference?

  • Web Development
  • Published
  • Updated
  • 4 min read
MongoDB Schema Design: Embed or Reference?

MongoDB lets you store data any way you like, which is exactly the problem. Coming from SQL, people either normalise everything — losing the point of a document database — or embed everything and hit the document size limit a year later.

Almost every schema decision reduces to one question: embed or reference?

The core rule

Model around how you read the data, not how it relates conceptually. In SQL you design for correctness and join at read time. In MongoDB you design for the queries you will actually run.

Ask: what does this screen need in one query?

Embed when

  • The child data is always read together with the parent
  • The child does not exist independently — an address belongs to one user
  • The relationship is one-to-few, not one-to-many-thousands
  • The child data changes rarely
// Good embed: a user and their addresses
{
  _id: ObjectId("..."),
  name: "Ayesha Khan",
  email: "ayesha@example.com",
  addresses: [
    { label: "Home", city: "Lahore", postal: "54600" },
    { label: "Office", city: "Lahore", postal: "54000" }
  ]
}

One query returns everything the profile page needs. No joins, no second round trip.

Reference when

  • The child is queried independently — you list orders without their customer
  • The relationship is one-to-many with no realistic ceiling: a user and their orders
  • The child is shared by multiple parents
  • The child changes often and is read in many places, so duplicating it would mean updating it everywhere
// Good reference: orders point at a user
{
  _id: ObjectId("..."),
  userId: ObjectId("..."),
  total: 4500,
  status: "shipped",
  items: [
    { productId: ObjectId("..."), name: "Blue Kurta", qty: 2, price: 2250 }
  ]
}

Note that the order items are embedded — they belong to this order and nothing else — while the user is referenced. Mixed strategies within one document are normal and correct.

Deliberate duplication

This feels wrong coming from SQL, but copying a small amount of data is often correct. If an order list shows the customer name, storing the name on the order avoids a lookup for every row.

The trade-off is that a name change does not propagate. For an order, that is actually desirable — it should record the name as it was at purchase time. For a live profile, it is not. Decide case by case.

Indexes: the difference between fast and unusable

An unindexed query scans every document in the collection. This is invisible at a thousand documents and fatal at a million.

  • Index every field you filter or sort by
  • For queries filtering on multiple fields, a compound index beats several single ones
  • Compound index order matters: equality fields first, then sort fields, then range fields
  • Unique indexes for things that must be unique — email, username
  • Do not index everything. Each index costs write performance and memory.

Use explain() on your slow queries. If the plan says COLLSCAN, you are missing an index.

Mongoose specifics

  • Define schemas even though MongoDB does not require them — the validation catches real bugs
  • Use lean() for read-only queries; it skips hydrating full documents and is meaningfully faster
  • select() only the fields you need instead of returning whole documents
  • Be careful with populate() in loops — it is the MongoDB version of the N+1 problem
  • Set timestamps: true rather than managing createdAt and updatedAt yourself

Mistakes that surface later

  1. Unbounded embedded arrays. Comments, logs, activity feeds — all grow forever.
  2. No indexes until it is slow. By then the collection is large and index builds are painful.
  3. Storing numbers as strings, then being unable to sort or compare them properly.
  4. Deeply nested documents four levels down, which are miserable to query and update.
  5. Treating MongoDB as a relational database with joins bolted on. If your data is genuinely relational and transactional, PostgreSQL may simply be the right tool.

A decision checklist

For each relationship, ask in order:

  1. Do I ever need the child without the parent? If yes, reference.
  2. Can this array grow without a realistic limit? If yes, reference.
  3. Is it always read together and bounded? If yes, embed.
  4. Is it read often, changed rarely, and small? Consider duplicating it.

Get this right early. Schema migrations on a live collection are the single least enjoyable task in backend work.

Need help building this?

I take on web app, mobile and e-commerce projects. Tell me what you are building and I will reply within 24 hours with scope, timeline and a fixed quote.

Start a project