Good MongoDB schema design starts from your queries, not from your entity diagram. This is the part that trips up people arriving from relational databases: the instinct is to normalize first and figure out the reads later. In MongoDB you do the opposite. You look at the reads your application makes most, then shape the documents so those reads hit one collection and come back whole. Model for how you access the data, and the schema mostly designs itself.
The one decision that matters: embed or reference
Almost every MongoDB modeling question reduces to this. Do you nest related data inside the parent document, or store it in its own collection and link by id?
Embed when the related data is read with the parent, is bounded, and does not change on its own:
// An order and its line items, read together, always
{
_id: ObjectId("..."),
userId: 123,
status: "shipped",
items: [
{ sku: "A-1", qty: 2, price: 1999 },
{ sku: "B-7", qty: 1, price: 4500 }
],
total: 8498
}
One read gets the order and everything on it. No join, no second round trip.
Reference when the related set is large, unbounded, shared, or changes independently:
// A user, and their events in a separate collection
{ _id: 123, email: "sam@example.com", plan: "pro" }
// events collection
{ _id: ObjectId("..."), userId: 123, type: "login", at: ISODate("...") }
You would not embed a user's events, because there is no ceiling on how many there are, and a document has a ceiling. The MongoDB data model design guide covers the trade-offs in depth, but access pattern is the deciding question every time.
Watch the ceiling: 16 MB and unbounded arrays
A MongoDB document maxes out at 16 MB. That is a lot of room, and it is also a design signal. If a document can grow toward that limit, it is almost always because of an array with no upper bound: every comment on a post, every event for a user, every message in a channel that has been busy since 2019.
An array that only grows is a schema smell. It makes the document heavier on every read, even when you only wanted three fields off the top, and one day it hits the wall. When you spot an unbounded array, that is the data telling you to reference it in its own collection. Bounded arrays (an order's line items, a document's tags) are exactly what embedding is for. The line is "does this have a natural limit," not "is this a list."
Plan for change with a schema version
Your schema will change, because your product will. The clean way to handle that is a version field on the document:
{ _id: 123, schemaVersion: 2, email: "sam@example.com", fullName: "Sam Rivers" }
New writes carry the current version. Old documents keep their old version until a migration touches them, and your application can read both shapes in the meantime. It beats a big-bang migration that has to rewrite every document in one window and be perfect on the first try. (It never is. This is the schema-design equivalent of "we will fix it in prod," except it actually works.)
Enforce the shape once it settles
A flexible schema is a gift while you are still figuring out the model, and a liability once you are not. When the design has settled, lock it in with validation so drift cannot creep back:
db.runCommand({
collMod: "orders",
validator: { $jsonSchema: {
bsonType: "object",
required: ["userId", "status", "total"],
properties: {
userId: { bsonType: "int" },
status: { enum: ["pending", "shipped", "cancelled"] },
total: { bsonType: "int", minimum: 0 }
}
} }
})
Now a write that forgets total or sends status: "shpped" is rejected instead of quietly stored. The schema validation docs cover the full operator set, including how to warn during a migration instead of hard-rejecting.
Design once, then watch for drift
The catch with a flexible schema is that even a good design drifts under real traffic: an upstream change starts writing a string where you modeled an integer, and the query that filters on it silently stops matching. That is why designing the schema and analyzing it are two different jobs, both worth doing. Once your collections are live, analyzing your MongoDB schema is how you catch the drift, and a compound index on the fields you filter and sort keeps the reads you designed for fast (see creating an index).
mongoui's schema doctor samples a collection and reports the real field shapes, type drift, and missing fields, so you can see whether the data still matches the model you designed. It runs on your machine, so the documents it samples never leave your network. Design for your queries, enforce the shape, and check now and then that reality still agrees with the diagram.
