mongoui
Schema·July 3, 2026·7 min read

How to design a MongoDB schema for the queries you actually run

Good MongoDB schema design starts from your queries, not from normal forms. The real decision is embed versus reference, and it is answered by how you read the data. Here is the framework, with the trade-offs that bite later.

Arctic Glass illustration for How to design a MongoDB schema for the queries you actually run

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.

Frequently asked questions

How do I design a schema in MongoDB?

Start from your queries, not from a normalized table diagram. List the reads your application does most, then shape documents so those reads hit one collection without a join. The main lever is embedding related data versus referencing it by id, and access pattern decides which.

When should I embed versus reference in MongoDB?

Embed when the related data is read together with the parent, is bounded in size, and does not change independently, for example order line items inside an order. Reference when the related set is large or unbounded, is shared across parents, or changes on its own, for example a user's activity events.

What is the maximum document size in MongoDB?

16 MB per document. That limit is also a design signal: if a document can grow toward it, usually through an unbounded array, that data should be referenced in its own collection instead of embedded.

How do I enforce a schema in MongoDB?

Attach a $jsonSchema validator to the collection with collMod or at creation. MongoDB then rejects writes that do not match the required fields and types. It turns a flexible schema into an enforced one once your design has settled.