Photo by Yosuke Ota on Unsplash

On this page

This chapter follows CRUD operations in MongoDB. MongoDB does not require a schema upfront, but your access patterns should drive how you nest, duplicate, and index data.

Embedding versus referencing

Embedding stores related data inside one document. Reads are single-round-trip and atomic for that document. Referencing stores an ObjectId (or key) and joins at read time with a second query or $lookup.

Pattern Prefer embedding when Prefer referencing when
One-to-few Child count is bounded (addresses, tags) —
One-to-many Children are read with parent and stay small Children grow without bound
Many-to-many Rare; duplicate ids both sides carefully Use junction collection of ids

Example embedded order line items:

{
"_id": { "$oid": "..." },
"customerId": "...",
"items": [
{ "sku": "A1", "qty": 2, "price": 9.99 },
{ "sku": "B2", "qty": 1, "price": 4.5 }
]
}

Example referenced product catalog:

{ "_id": { "$oid": "..." }, "name": "Widget", "vendorId": { "$oid": "..." } }

One-to-many and many-to-many

For one-to-many with unbounded children (comments on a post, events in a log), use a separate comments collection with { postId, createdAt, body } and index { postId: 1, createdAt: -1 } rather than an ever-growing array on posts.

For many-to-many, a junction collection { userId, groupId, joinedAt } with a compound unique index on (userId, groupId) avoids duplicating large arrays on both sides.

Design patterns

Extended reference copies a few stable fields (for example authorName) alongside authorId to avoid a join on list views, accepting stale names until you backfill.

Subset embeds only the latest N related items (recent messages) and archives the rest elsewhere.

Computed stores denormalized totals (orderTotal) updated by application logic or aggregation on write.

Bucket groups time-series measurements into one document per hour with an array of samples—pairs well with time series collections.

Outlier uses two shapes in one collection (most documents small, rare huge ones split to another collection) when variance is extreme.

Anti-patterns

  • Massive unbounded arrays on hot documents: every push rewrites the whole document and hits the 16 MB limit.
  • Deep nesting that mirrors arbitrary JSON from APIs: hard to index and update partial fields.
  • Polymorphic documents without a discriminator field: queries cannot use indexes efficiently.
  • Joining everything at read when embedding would have matched access patterns: latency adds up.

The 16 MB document limit

BSON document size is capped at 16 MB. GridFS splits larger blobs across chunks; see the MongoDB glossary for GridFS. If individual logical records approach megabytes, split them.

JSON Schema validation

You can enforce shape at insert/update with $jsonSchema:

db.createCollection('users', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['email', 'createdAt'],
properties: {
email: { bsonType: 'string', pattern: '^[^@]+@[^@]+\\.[^@]+$' },
createdAt: { bsonType: 'date' },
age: { bsonType: 'int', minimum: 0 },
},
},
},
validationLevel: 'moderate',
})

validationLevel: 'moderate' validates updates only when they touch fields covered by the schema; existing non-conforming documents can remain until edited.

From TypeScript, run the same command via db.command() during migrations.

Next: Mongoose with MongoDB.

Check your understanding

Question 1 of 5

0 of 5 checked

  1. 1. When does the article recommend embedding related data?

Search articles

Type a word to search published articles.

Open the search page