Skip to main content

πŸƒ 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:

MongoDB's data hierarchy A database contains collections; a collection contains documents; a document contains fields, which are key-value pairs. Database β€” "ecommerce" Collection: products { _id: ObjectId("a1"),   name: "Laptop", price: 999 } ↑ a document (fields = key/value pairs) { _id: ObjectId("a2"),   name: "Phone", color: "black" } ↑ different fields, same collection Collection: orders { _id: ObjectId("b1"),   customerId: 1001,   items: [     { prodId: "a1", qty: 1 }   ] } ↑ an array of embedded documents
Figure 1 β€” A database holds collections, a collection holds documents, and a document holds fields. Notice that two documents in the same collection can carry different fields.
Relational termMongoDB termWhat it is
DatabaseDatabaseTop-level container for collections
TableCollectionA group of related documents
RowDocumentA single record (BSON object)
ColumnFieldA key/value pair inside a document
Primary key_id fieldUnique identifier for the document
JOIN$lookup / referencePull 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:

ConceptRelational (SQL) β€” e.g. PostgreSQL/MySQLMongoDB (NoSQL)
Data structureTables of rows & columnsCollections of documents
SchemaFixed and enforced by the databaseFlexible; optionally enforced via schema validation
RelationshipsForeign keys + JOINsEmbedded documents or references
Query languageSQLMQL β€” a JSON-based query API
ScalingPrimarily vertical (bigger server)Horizontal via sharding, built in
TransactionsMulti-row ACID by defaultMulti-document ACID since v4.0
Best forComplex relationships, strict integrityFlexible/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 typeDescriptionExample
StringUTF-8 text"Hello, MongoDB!"
Int / Long32-bit or 64-bit integer42
Double64-bit floating point3.14159
Decimal128High-precision decimal (money)NumberDecimal("19.99")
Booleantrue / falsetrue
ArrayOrdered list of values["red", "green"]
ObjectEmbedded document{ name: "John" }
DateMilliseconds since epochISODate("2026-01-01")
ObjectId12-byte unique identifierObjectId("507f1f77...")
NullExplicit null valuenull
BinaryRaw binary dataBinData(...)

⚠️ 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:

Structure of a MongoDB ObjectId An ObjectId is 12 bytes: a 4-byte timestamp, a 5-byte random value unique to the machine and process, and a 3-byte incrementing counter. Timestamp 4 bytes β€” seconds since epoch Random value 5 bytes β€” machine + process Counter 3 bytes β€” increment 507f191e810c19729de860ea
Figure 2 β€” Because the leading bytes are a timestamp, ObjectIds sort roughly by creation time. That means _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 _id is 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.

flowchart TD Q{How is the data related
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 parentThe child is queried on its own
One-to-one or one-to-fewOne-to-many (large) or many-to-many
The embedded data won't grow without boundThe related set can grow very large
Fast reads matter mostAvoiding 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

  1. Decide whether the user's bio should be embedded or a separate collection.
  2. Decide how a post links to its author.
  3. Decide whether comments should be embedded in the post or live in their own collection β€” and justify it using the unbounded-array idea.
  4. Write an example document for at least the posts collection.
πŸ’‘ 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.