Data modelling: embedding vs referencing and schema validation
Decide when a child belongs inside the parent document, enforce the shape you settled on with JSON Schema, and evolve that shape without a stop-the-world migration.
Embed or reference
| Factor | Embed | Reference |
|---|---|---|
| Access pattern | Read with the parent every time | Read on its own, independently |
| Cardinality | One-to-few | One-to-many or many-to-many |
| Growth | Bounded and predictable | Unbounded over time |
| Sharing | Private to the parent | Shared between several parents |
| Write pattern | Updated with the parent | Updated on its own schedule |
| Consistency | One atomic write covers everything | Needs a transaction for atomicity |
// embedded: bounded, always read with the order
{
_id: ObjectId("66e0a1f2c3b4d5e6f7a8b9c0"),
customer: { id: ObjectId("..."), name: "Ada" },
items: [ { sku: "LAMP-01", qty: 2 }, { sku: "CABLE-3", qty: 1 } ],
status: "paid"
}
// referenced: unbounded, queried on its own
{
_id: ObjectId("..."),
orderId: ObjectId("66e0a1f2c3b4d5e6f7a8b9c0"),
at: ISODate("2026-09-14T09:12:00Z"),
event: "payment.authorised"
}- The document is the unit of atomicity. Modelling so that a single logical change fits in one document is both simpler and faster than reaching for a transaction.
- Duplicate the two or three fields you always need rather than joining for them — but treat the copy as a cache and know what refreshes it.
- An array that grows on every write is the most common modelling mistake. Bound it, bucket it, or move the children to their own collection.
JSON Schema validation
db.createCollection("orders", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["customer", "status", "placedAt"],
properties: {
customer: { bsonType: "objectId", description: "reference to customers" },
status: { enum: ["new", "paid", "shipped", "cancelled"] },
total: { bsonType: "decimal", minimum: 0 },
items: {
bsonType: "array", minItems: 1,
items: {
bsonType: "object",
required: ["sku", "qty"],
properties: { qty: { bsonType: "int", minimum: 1 } }
}
}
},
additionalProperties: true
}
},
validationLevel: "moderate", // strict or moderate
validationAction: "error" // error or warn
});
// tighten the rules later without recreating the collection
db.runCommand({ collMod: "orders", validationLevel: "strict" });validationLevel: "strict"applies to inserts and to every update;"moderate"only validates inserts and updates to already-valid documents, which lets pre-existing bad data stay while new writes are held to the rule.validationAction: "warn"records a violation in the log and allows the write. It is the right first step when you want to measure how much existing code would break.- Validators apply to the future, not the past. Documents already stored are never re-checked unless something updates them.
Evolving a schema safely
// version the shape, then migrate lazily in batches
db.orders.updateMany(
{ schemaVersion: { $lt: 2 } },
{ $set: { schemaVersion: 2, "shipping.method": "standard" } }
);
db.orders.createIndex({ schemaVersion: 1 });
db.orders.countDocuments({ schemaVersion: { $lt: 2 } }); // backlog remaining- Store a
schemaVersionand let the application read both generations while the migration runs. A field the old code ignores is safer than a field it misinterprets. - Additive changes are cheap; renaming or retyping a field is a deployment that must be ordered so that both versions of the code work against both versions of the document.
- Migrate in bounded batches. One unbounded
updateManyover a large collection holds locks and writes an enormous oplog entry. - A validator is a guard rail, not a migration tool. It rejects new writes that break the rule and says nothing at all about data already stored.
⚠️
An unbounded array is the mistake that eventually forces a migration.
$push grows the document on every write until it reaches the 16 MB limit, and by then the fix is a data migration. Bound the array, or move the children into their own collection with a bucket pattern.FAQ
Should every collection have a validator?
Every collection whose shape matters to correctness. A validator costs one comparison per write and catches the class of bug that otherwise shows up months later as a missing field in production. Use
warn first to measure existing violations, then switch to error.How large should a collection be?
There is no useful limit per collection; the limit that matters is per document. The practical question is whether a single document stays bounded and whether the working set of live documents fits in RAM.
Related
Documents, collections and queries Indexes and schema design patterns
Last refreshed 2026-09-18.