Documents & BSON
MongoDB stores data as BSON (Binary JSON) — a binary-encoded serialization of JSON-like documents. BSON was designed specifically for MongoDB to be lightweight, traversable, and efficient for both encoding and decoding.
BSON extends JSON with additional data types that JSON cannot represent natively: ObjectId, Date, Binary, Decimal128, Timestamp, RegExp, and more. This richer type system enables precise numeric arithmetic, efficient date range queries, and embedded binary data — all without the ambiguity of string encoding.
Every MongoDB document is a BSON object: an ordered set of key-value pairs stored as a contiguous binary blob.
What is a Document?
A document is the fundamental unit of data in MongoDB — equivalent to a row in a relational database, but far more flexible. Documents are self-describing: they carry their own field names alongside their values, and any document in a collection can have a different structure.
A complete MongoDB document
{
"_id": { "$oid": "64f1a2b3c4d5e6f7a8b9c0d1" },
"name": "Alice Müller",
"age": 28,
"email": "alice@example.com",
"verified": true,
"score": { "$numberDecimal": "9.87" },
"createdAt": { "$date": "2024-01-15T09:30:00.000Z" },
"tags": ["admin", "beta-tester", "power-user"],
"address": {
"street": "123 Main St",
"city": "Berlin",
"country": "DE",
"postalCode": "10115"
},
"profileImage": { "$binary": { "base64": "", "subType": "00" } },
"deletedAt": null
}BSON Data Types
BSON Type | JavaScript Type | Description | Example |
|---|---|---|---|
String | String | UTF-8 encoded text. The most common type for human-readable values. | "Alice" |
Integer (32-bit) | Number | Whole numbers up to ±2 147 483 647. MongoDB uses 32-bit by default for small integers. | 42 |
Integer (64-bit) | Number / BigInt | Whole numbers up to ±9.2 × 10¹⁸. Required for timestamps and large counters. | NumberLong("9007199254740993") |
Double | Number | 64-bit IEEE 754 floating-point. Default for fractional numbers in JavaScript. | 3.14 |
Decimal128 | Decimal128 | High-precision 128-bit decimal. Use for currency and financial calculations to avoid floating-point drift. | NumberDecimal("9.99") |
Boolean | Boolean | True or false. | true |
Date | Date | UTC milliseconds since Unix epoch. Always store dates as Date — never as strings. | new Date() |
ObjectId | ObjectId | 12-byte unique identifier auto-generated for _id fields. Embeds a creation timestamp. | ObjectId() |
Array | Array | Ordered list of values of any BSON type, including nested arrays and documents. | ["a", "b", 3] |
Object (Embedded Document) | Object | A nested BSON document. Enables denormalized, self-contained records. | { city: "Berlin" } |
Null | null | Explicit absence of value. Distinct from a missing field. | null |
Regular Expression | RegExp | A PCRE regex pattern with optional flags. Used in query filters. | /^alice/i |
Binary Data | Buffer / BinData | Raw bytes. Used for file contents, UUIDs, hashes, and encrypted payloads. | BinData(0, "...") |
Timestamp | Timestamp | 64-bit internal type used by the replication oplog. Not for application use — use Date instead. | Timestamp() |
Symbol | Symbol | Deprecated. Identical to String for modern drivers. Avoid in new schemas. | — |
MinKey / MaxKey | MinKey / MaxKey | Special comparison values that sort lower/higher than all other BSON types. Used in sharding. | MinKey() |
The _id Field
Every MongoDB document must have an _id field, which serves as the document's primary key. The _id field:
- Must be unique within its collection.
- Is immutable — you cannot change it after insertion.
- Is automatically indexed — MongoDB creates a unique index on
_idfor every collection. - Is auto-generated as a 12-byte
ObjectIdif you don't supply one.
ObjectId structure
// Create a new ObjectId
const id = new ObjectId()
id.toString() // "64f1a2b3c4d5e6f7a8b9c0d1" (24 hex chars)
id.getTimestamp() // Date object representing when the ObjectId was generated
// An ObjectId encodes:
// ┌──────────────┬─────────────────────┬──────────────────┐
// │ 4 bytes │ 5 bytes │ 3 bytes │
// │ Unix epoch │ Random (process + │ Auto-increment │
// │ (seconds) │ machine entropy) │ counter │
// └──────────────┴─────────────────────┴──────────────────┘
//
// The timestamp component means ObjectIds sort chronologically
// and you can extract an approximate creation time for free.
// Extract timestamp from an existing ObjectId
ObjectId("64f1a2b3c4d5e6f7a8b9c0d1").getTimestamp()
// ISODate("2023-09-01T10:22:43.000Z")Custom _id Values
You are not required to use ObjectId — any BSON value that is unique within the collection is valid as an _id:
Documents with custom _id
// String _id — useful when a natural key exists (e.g., username, ISO code)
db.countries.insertOne({ _id: "DE", name: "Germany", population: 83_200_000 })
// Integer _id — suitable for small, controlled datasets
db.settings.insertOne({ _id: 1, theme: "dark", language: "en" })
// UUID _id — for systems that generate UUIDs externally
const { UUID } = require("bson")
db.sessions.insertOne({ _id: new UUID("550e8400-e29b-41d4-a716-446655440000"), userId: "abc" })
// Composite _id — an embedded document acting as a compound primary key
db.inventory.insertOne({
_id: { warehouse: "NYC", sku: "WIDGET-42" },
quantity: 500,
reserved: 12
})_id field is immutable after insertion. If you need to change a document's identifier, you must delete the old document and insert a new one. Attempting to $set the _id field throws an ImmutableField error. Inserting a duplicate _id throws E11000 duplicate key error.Embedded Documents (Sub-documents)
Documents can contain other documents nested to any depth. This is the core strength of the document model: instead of joining multiple tables at query time, you store related data together in a single document.
Embedding is ideal when the nested data is:
- Always accessed together with the parent document.
- Owned exclusively by one parent (no sharing).
- Finite and bounded in size.
Nested document example
db.orders.insertOne({
_id: ObjectId(),
orderNumber: "ORD-2024-00142",
status: "shipped",
// Embedded customer — no join needed to display the order
customer: {
_id: ObjectId("64abc123..."),
name: "Alice Müller",
email: "alice@example.com"
},
// Embedded shipping address — snapshot at time of order
shippingAddress: {
street: "123 Main St",
city: "Berlin",
postalCode: "10115",
country: "DE"
},
// Array of embedded line items
items: [
{ sku: "WIDGET-42", name: "Blue Widget", qty: 2, unitPrice: 9.99 },
{ sku: "GADGET-7", name: "Red Gadget", qty: 1, unitPrice: 24.50 }
],
subtotal: 44.48,
tax: 8.45,
total: 52.93,
createdAt: new Date(),
shippedAt: null
})Arrays in Documents
Arrays are first-class citizens in MongoDB. You can store ordered lists of any BSON type — strings, numbers, embedded documents, or even other arrays — directly within a document field. MongoDB provides a rich set of array query and update operators specifically for working with array fields.
Array field types
// Array of strings
{ tags: ["javascript", "nodejs", "mongodb"] }
// Array of numbers
{ scores: [95, 87, 92, 78, 100] }
// Array of embedded documents (the most powerful pattern)
{
comments: [
{ author: "Bob", text: "Great post!", likes: 5, postedAt: ISODate("2024-01-10") },
{ author: "Carol", text: "Very helpful.", likes: 12, postedAt: ISODate("2024-01-11") }
]
}
// Mixed-type array (valid BSON, but query complex — avoid in practice)
{ misc: [42, "hello", true, null, { key: "val" }] }
// Querying into arrays
db.posts.find({ tags: "nodejs" }) // match if array contains "nodejs"
db.posts.find({ scores: { $gt: 90 } }) // match if any element > 90
db.posts.find({ "comments.author": "Bob" }) // dot notation into embedded array docsDocument Size Limits
fs.files and fs.chunks).Limit | Value | Notes |
|---|---|---|
Max document size | 16 MB | Includes all embedded documents and array elements. Use GridFS for larger binary data. |
Max BSON nesting depth | 100 levels | Deeply nested structures degrade query performance and readability. Keep nesting shallow. |
Max field name length | No hard limit | Field names are stored in every document. Shorter names (e.g., "ts" vs "timestamp") save significant space in large collections. |
Max index key size | 1 024 bytes | Strings used as index keys are subject to this limit (pre-4.2 stricter limits may apply). |
Max documents per collection | Unlimited (practical limit: billions) | Performance depends on indexes, hardware, and access patterns — not a hard document count cap. |
Working with Dates
MongoDB stores dates as 64-bit integers representing milliseconds since the Unix epoch (UTC). The Date type supports native comparison operators, sorting, aggregation arithmetic, and TTL indexes (automatic document expiration).
Date handling in MongoDB
// Storing dates — always use new Date() or ISODate()
db.events.insertOne({
name: "Conference",
startsAt: new Date("2024-06-15T09:00:00Z"),
endsAt: ISODate("2024-06-17T18:00:00Z"),
createdAt: new Date() // current UTC time
})
// Querying by date range
db.events.find({
startsAt: {
$gte: new Date("2024-01-01"),
$lt: new Date("2025-01-01")
}
})
// Sorting chronologically (ascending = earliest first)
db.events.find().sort({ startsAt: 1 })
// Extracting date parts in aggregation
db.events.aggregate([
{
$project: {
year: { $year: "$startsAt" },
month: { $month: "$startsAt" },
day: { $dayOfMonth: "$startsAt" }
}
}
])new Date() or ISODate() for date fields — never store dates as strings. Proper Date types enable range queries, chronological sorting, TTL index expiration, and aggregation date arithmetic. String dates require complex parsing and sort lexicographically, not chronologically.