π MongoDB Document Structure
Relational databases store data in rigid rows and columns. MongoDB takes a different path: it stores rich, self-describing documents that look a lot like the JavaScript objects you already work with. This lesson gives you a solid mental model of the document, the collection, and the design decisions that make or break a NoSQL schema.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain the document model and how databases, collections, documents, and fields relate
- Describe what BSON is and name the common data types a document can hold
- Interpret a MongoDB ObjectId and know when to supply your own
_id - Decide when to embed related data versus reference it, using clear rules of thumb
- Model one-to-one, one-to-many, and many-to-many relationships in a document database
Estimated Time: 30β40 minutes β’ Difficulty: Intermediate
Hands-on: Design the document schema for a small blogging platform and justify each embed/reference choice.
In This Lesson
What Is MongoDB?
MongoDB is an open-source NoSQL document database. Instead of splitting your data across tables of rows and columns, it stores each record as a single, flexible document β a structure that reads almost exactly like a JSON object. That single design choice ripples through everything else: the query language, how you model relationships, and how the database scales.
π‘ Filing cabinet vs. spreadsheet. A relational database is like a spreadsheet: every row in a sheet must share the same rigid set of columns. MongoDB is more like a filing cabinet of folders β each folder (collection) holds documents, and each document can carry exactly the fields it needs, even if its neighbor carries different ones.
This flexibility is why MongoDB shines for content management, catalogs with wildly varying attributes, real-time analytics, rapidly evolving products, and IoT/time-series workloads. It is not a magic replacement for everything β heavily relational data with many joins and strict, database-enforced integrity often still fits a relational engine like PostgreSQL better. Most professional teams end up using both, choosing per workload.
π Key Terms
NoSQL: a family of databases that do not use the relational table model β document, key-value, column-family, and graph stores all count.
Document: a single record stored as a set of field/value pairs, physically encoded as BSON.
Collection: a group of related documents; the rough equivalent of a table.
The Document Hierarchy
MongoDB organizes data into four nested levels. Understanding this hierarchy is the fastest way to translate what you already know about relational databases:
| Relational term | MongoDB term | What it is |
|---|---|---|
| Database | Database | Top-level container for collections |
| Table | Collection | A group of related documents |
| Row | Document | A single record (BSON object) |
| Column | Field | A key/value pair inside a document |
| Primary key | _id field | Unique identifier for the document |
| JOIN | $lookup / reference | Pull related data together |
MongoDB vs. Relational Databases
The document model is a genuine trade-off, not a strict upgrade. Here is a fair side-by-side so you can reason about which tool fits a given problem:
| Concept | Relational (SQL) β e.g. PostgreSQL/MySQL | MongoDB (NoSQL) |
|---|---|---|
| Data structure | Tables of rows & columns | Collections of documents |
| Schema | Fixed and enforced by the database | Flexible; optionally enforced via schema validation |
| Relationships | Foreign keys + JOINs | Embedded documents or references |
| Query language | SQL | MQL β a JSON-based query API |
| Scaling | Primarily vertical (bigger server) | Horizontal via sharding, built in |
| Transactions | Multi-row ACID by default | Multi-document ACID since v4.0 |
| Best for | Complex relationships, strict integrity | Flexible/evolving data, fast iteration, scale-out |
β Choosing well
Reach for MongoDB when your data is hierarchical, your schema is still moving, or you need to scale writes horizontally. Reach for a relational database when your data is highly relational, you rely on complex multi-table joins, or database-enforced constraints are non-negotiable. The good news: the concepts you learn here β querying, indexing, modeling β transfer to both worlds.
Documents and BSON Types
Although you read and write documents as JSON, MongoDB stores them on disk as BSON β Binary JSON. BSON is a binary-encoded superset of JSON that adds data types JSON lacks (dates, 64-bit integers, binary data, ObjectId, and more) and is designed to be fast to traverse. A single BSON document can be up to 16 MB; larger blobs belong in GridFS or object storage.
Here is a realistic user document that shows nesting, arrays, and several BSON types working together:
{
"_id": ObjectId("5f8a76e910bd12b4e4c9a0f1"), // ObjectId
"username": "johndoe", // String
"email": "john@example.com", // String
"isActive": true, // Boolean
"loginCount": 42, // Int
"profile": { // Embedded document
"firstName": "John",
"lastName": "Doe",
"birthDate": ISODate("1990-07-15T00:00:00Z"), // Date
"address": { // Nested embedded document
"city": "New York",
"zipCode": "10001"
}
},
"interests": ["programming", "hiking"], // Array of strings
"createdAt": ISODate("2020-10-17T09:34:33Z") // Date
}
| BSON type | Description | Example |
|---|---|---|
| String | UTF-8 text | "Hello, MongoDB!" |
| Int / Long | 32-bit or 64-bit integer | 42 |
| Double | 64-bit floating point | 3.14159 |
| Decimal128 | High-precision decimal (money) | NumberDecimal("19.99") |
| Boolean | true / false | true |
| Array | Ordered list of values | ["red", "green"] |
| Object | Embedded document | { name: "John" } |
| Date | Milliseconds since epoch | ISODate("2026-01-01") |
| ObjectId | 12-byte unique identifier | ObjectId("507f1f77...") |
| Null | Explicit null value | null |
| Binary | Raw binary data | BinData(...) |
β οΈ Money and floating point
Never store currency as a Double β floating-point rounding will bite you (0.1 + 0.2 !== 0.3). Use Decimal128 (NumberDecimal("19.99")) or store integer cents. This same rule applies in every language and database, not just MongoDB.
The _id Field and ObjectIds
Every document must have a unique _id field β it is the primary key. If you do not supply one, MongoDB generates an ObjectId automatically. An ObjectId is a 12-byte value, usually shown as a 24-character hex string, and it is cleverly structured:
_id alone often gives you free chronological ordering.- Globally unique: generated so different servers never collide.
- Time-sortable: the first 4 bytes encode creation time, so sorting by
_idis close to sorting by "createdAt". - Custom IDs allowed: you may set your own
_id(e.g. an email or SKU) when you already have a natural key.
const { ObjectId } = require('mongodb');
// Generate a new ObjectId
const newId = new ObjectId();
console.log(newId.toString()); // "507f191e810c19729de860ea"
// Extract the creation timestamp
console.log(newId.getTimestamp()); // 2026-01-01T12:34:56.000Z
// Re-create an ObjectId from a stored string (e.g. from a URL param)
const existingId = new ObjectId("507f191e810c19729de860ea");
π‘ A common bug
A value that looks like an ObjectId in a URL (/users/507f191e...) is a string, not an ObjectId. Querying { _id: "507f191e..." } will silently match nothing. Always wrap it: { _id: new ObjectId(req.params.id) }.
Embedding vs. Referencing
This is the single most important schema-design decision in MongoDB. When two pieces of data are related, you can either embed one inside the other, or store them separately and reference by _id.
and accessed?} --> E[Embedding] Q --> R[Referencing] E --> E1[One query fetches everything] E --> E2[Atomic single-document updates] E --> E3[Risk: 16MB limit & duplication] R --> R1[No duplication, smaller docs] R --> R2[Good for many-to-many] R --> R3[Cost: extra queries / joins]
Embedding: store related data together
// A user with their addresses embedded β one read gets it all
{
"_id": ObjectId("..."),
"username": "johndoe",
"addresses": [
{ "type": "home", "city": "New York", "zipCode": "10001" },
{ "type": "work", "city": "New York", "zipCode": "10002" }
]
}
Referencing: store related data separately
// The user only stores IDs; addresses live in their own collection
{
"_id": ObjectId("u1"),
"username": "johndoe",
"addressIds": [ ObjectId("a1"), ObjectId("a2") ]
}
// addresses collection
{ "_id": ObjectId("a1"), "userId": ObjectId("u1"), "type": "home", "city": "New York" }
{ "_id": ObjectId("a2"), "userId": ObjectId("u1"), "type": "work", "city": "New York" }
| Embed when⦠| Reference when⦠|
|---|---|
| The relationship is "contains" / "part of" | The relationship is "refers to" |
| The child is always read with the parent | The child is queried on its own |
| One-to-one or one-to-few | One-to-many (large) or many-to-many |
| The embedded data won't grow without bound | The related set can grow very large |
| Fast reads matter most | Avoiding duplication matters most |
β οΈ The unbounded-array trap. Embedding comments inside a blog post feels natural β until a viral post gets 50,000 comments and blows past the 16 MB document limit. When a "many" side can grow without a clear ceiling, reference it instead.
Relationship Patterns
Most schemas are built from a handful of recurring patterns. Here is how each maps onto documents.
One-to-one β embed
{
"_id": ObjectId("..."),
"username": "johndoe",
"profile": { "bio": "Software developer", "avatarUrl": "/img/jd.png" }
}
One-to-few β embed an array
{
"_id": ObjectId("..."),
"name": "ACME Inc.",
"contacts": [
{ "name": "John Doe", "role": "CEO", "email": "john@acme.com" },
{ "name": "Jane Smith", "role": "CFO", "email": "jane@acme.com" }
]
}
One-to-many β reference (child points to parent)
// authors collection
{ "_id": ObjectId("author123"), "name": "Stephen King" }
// books collection β each book references its author
{ "_id": ObjectId("book1"), "title": "The Shining", "authorId": ObjectId("author123") }
{ "_id": ObjectId("book2"), "title": "It", "authorId": ObjectId("author123") }
Many-to-many β reference from one (or both) sides
// students collection
{ "_id": ObjectId("s1"), "name": "Alice", "courseIds": [ ObjectId("c1"), ObjectId("c2") ] }
// courses collection
{ "_id": ObjectId("c1"), "name": "Database Design" }
{ "_id": ObjectId("c2"), "name": "Web Security" }
β Rule of thumb
Model for how the application reads the data, not merely for how the entities relate on paper. If a screen always shows a user and their profile together, embed the profile. If a screen lists all books regardless of author, keep books in their own collection and reference the author.
Hands-on Exercise
ποΈ Design a Blog Schema
Objective: Model a small blogging platform and defend every embed-vs-reference choice.
The entities
- Users β each has a name, email, and a short bio.
- Posts β each has a title, body, author, tags, and comments.
- Comments β each has author text and a timestamp; popular posts may attract thousands.
Instructions
- Decide whether the user's bio should be embedded or a separate collection.
- Decide how a post links to its author.
- Decide whether comments should be embedded in the post or live in their own collection β and justify it using the unbounded-array idea.
- Write an example document for at least the
postscollection.
π‘ Hint
Ask two questions for each relationship: "Is this always read together with its parent?" and "Can the many-side grow without bound?" A bio is small and one-to-one; comments are potentially unbounded.
β Sample solution
Bio: embed β it's one-to-one, small, and always shown with the user.
Author: reference β a post refers to a user, and users are queried independently.
Comments: reference in their own collection β they can grow without bound and would risk the 16 MB limit if embedded.
// users
{ "_id": ObjectId("u1"), "name": "Ray", "email": "ray@example.com",
"profile": { "bio": "Full stack learner." } }
// posts (author referenced, tags embedded, comments referenced)
{ "_id": ObjectId("p1"), "title": "Hello Mongo",
"body": "My first post...", "authorId": ObjectId("u1"),
"tags": ["mongodb", "nosql"], "createdAt": ISODate("2026-01-01") }
// comments (each points back to its post and author)
{ "_id": ObjectId("cm1"), "postId": ObjectId("p1"),
"authorId": ObjectId("u1"), "text": "Great start!",
"createdAt": ISODate("2026-01-02") }
Best Practices
β Do
- Model your schema around your application's read patterns.
- Embed one-to-one and one-to-few data that is always read together.
- Reference data when the "many" side can grow without bound.
- Use Decimal128 or integer cents for money.
- Wrap id strings in
new ObjectId(...)before querying_id. - Add schema validation rules when you want the database to enforce structure.
β οΈ Avoid
- Embedding arrays that can grow indefinitely (comments, logs, events).
- Treating "schema-less" as "no design" β a bad document model is hard to fix later.
- Storing currency as floating-point
Double. - Over-normalizing into many collections and then rebuilding joins in application code.
Summary & Quiz
π Key Takeaways
- MongoDB stores data as flexible BSON documents grouped into collections.
- The hierarchy is database β collection β document β field, mapping loosely onto database β table β row β column.
- Every document has a unique
_id; MongoDB supplies a time-sortable ObjectId by default. - The core design choice is embed vs. reference β driven by how data is read and whether the "many" side is bounded.
- Model for access patterns, and never store money as a floating-point double.
π― Quick Quiz
Question 1: In MongoDB, what is the rough equivalent of a table in a relational database?
Question 2: You are modeling blog posts. Comments can number in the tens of thousands on popular posts. What's the best approach?
Question 3: What information is encoded in the first four bytes of an ObjectId?
π Further Reading
π What's Next?
Now that you can model documents, the next lesson puts them to work: CRUD Operations in MongoDB β inserting, querying, updating, and deleting documents with the Node.js driver.
π Nice work!
You can now read a document, name its parts, and defend a schema. Let's start manipulating data.