Data Modeling
MongoDB is a document database, and how you shape your documents matters far more than it does in SQL. The golden rule: model your data around your application's access patterns, not around normalization. A single document read is cheaper than a multi-collection join, so co-locate data that is always read together and separate data that is queried independently.
Embedding vs Referencing
Every modeling decision comes down to one question: should this data live inside the parent document (embed), or in its own collection with a reference (like a foreign key)?
Factor | Embed | Reference |
|---|---|---|
Accessed together? | Yes — always read together | No — queried independently |
Changes together? | Yes — updated atomically | No — updated separately |
Data size | Small (document stays <16 MB) | Large or unbounded |
1:1 relationship | Embed | Either |
1:few relationship | Embed | Either |
1:many relationship | Either | Reference |
Many:many relationship | Rarely | Reference |
One-to-One: Embed It
When two entities always appear together and neither grows unboundedly, embed one inside the other.
// ✅ Embedded: user + profile in one document
{
_id: ObjectId("..."),
name: "Alice",
email: "alice@example.com",
profile: {
bio: "Software engineer",
avatarUrl: "https://cdn.example.com/alice.jpg",
location: "Berlin"
}
}
// ❌ Separate collections — forces a second query every time
// users collection: { _id, name, email }
// profiles collection: { userId, bio, avatarUrl, location }One-to-Few: Embed It
// ✅ Order with embedded line items (bounded array)
{
_id: ObjectId("..."),
orderNumber: "ORD-1042",
customerId: ObjectId("..."),
placedAt: ISODate("2024-03-15T10:00:00Z"),
lineItems: [
{ productId: ObjectId("..."), name: "Keyboard", qty: 1, unitPrice: 89.99 },
{ productId: ObjectId("..."), name: "Mouse", qty: 2, unitPrice: 29.99 },
{ productId: ObjectId("..."), name: "USB Hub", qty: 1, unitPrice: 19.99 }
],
total: 169.96
}One-to-Many: Reference It
When the "many" side can grow without bound (e.g. comments on a post), store references instead of embedding — otherwise the parent document will balloon past the 16 MB limit.
// posts collection
{
_id: ObjectId("post1"),
title: "Getting Started with MongoDB",
body: "...",
authorId: ObjectId("user1"),
commentIds: [
ObjectId("comment1"),
ObjectId("comment2"),
ObjectId("comment3")
]
}
// comments collection (each comment references its post)
{
_id: ObjectId("comment1"),
postId: ObjectId("post1"),
authorId: ObjectId("user2"),
text: "Great article!",
createdAt: ISODate("2024-03-16T09:00:00Z")
}
// Fetch post then its comments
const post = await db.collection('posts').findOne({ _id: postId })
const comments = await db.collection('comments')
.find({ _id: { $in: post.commentIds } })
.toArray()Many-to-Many: Reference It
Store an array of references on both sides of the relationship. Neither collection embeds the other.
// students collection
{
_id: ObjectId("student1"),
name: "Bob",
courseIds: [ObjectId("course1"), ObjectId("course2")]
}
// courses collection
{
_id: ObjectId("course1"),
title: "Introduction to Databases",
studentIds: [ObjectId("student1"), ObjectId("student3"), ObjectId("student7")]
}
// Find all courses for a student
const student = await db.collection('students').findOne({ _id: studentId })
const courses = await db.collection('courses')
.find({ _id: { $in: student.courseIds } })
.toArray()The Subset Pattern
When you only ever display the most recent N items (e.g. the last 5 reviews on a product page), keep only that subset embedded in the parent document. Store the full list in a separate collection. This avoids loading thousands of subdocuments for a summary widget.
// products collection — subset of reviews embedded
{
_id: ObjectId("prod1"),
name: "Mechanical Keyboard",
price: 129.99,
reviewCount: 342,
avgRating: 4.7,
recentReviews: [
{ userId: ObjectId("..."), rating: 5, text: "Love it!", date: ISODate("2024-03-14") },
{ userId: ObjectId("..."), rating: 4, text: "Solid build.", date: ISODate("2024-03-13") },
{ userId: ObjectId("..."), rating: 5, text: "Fast shipping.",date: ISODate("2024-03-12") },
{ userId: ObjectId("..."), rating: 3, text: "A bit loud.", date: ISODate("2024-03-11") },
{ userId: ObjectId("..."), rating: 5, text: "Perfect.", date: ISODate("2024-03-10") }
]
}
// reviews collection — full history
{
_id: ObjectId("rev1"),
productId: ObjectId("prod1"),
userId: ObjectId("..."),
rating: 5,
text: "Love it!",
date: ISODate("2024-03-14")
}Document Size Limit
Schema Validation
MongoDB is schemaless by default, but you can enforce a schema at the collection level using JSON Schema validation. Invalid documents are rejected at write time.
db.createCollection("users", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["name", "age", "email"],
properties: {
name: {
bsonType: "string",
description: "must be a string and is required"
},
age: {
bsonType: "int",
minimum: 0,
maximum: 120,
description: "must be an integer between 0 and 120"
},
email: {
bsonType: "string",
description: "must be a valid email address"
}
}
}
},
validationAction: "error"
})