Skip to main content

Document Databases

Level 2: Level 2: Data Storage & Modelingmedium25 mindocument-dbmongodbmodeling

Embed bounded read-together data, reference large or unbounded entities, respect the 16MB cap, and treat per-document atomicity as a design constraint.

Schema-on-read trees, and the embed-vs-reference call

A document database stores semi-structured records, typically JSON or its binary form BSON, where each document is a self-contained tree: nested objects, arrays, and scalars. MongoDB, Couchbase, and Firestore are the common examples. Unlike a relational table, there is no fixed schema enforced by the engine. It is schema-on-read: two documents in the same collection can have different fields, and the application interprets shape at read time. That flexibility is the selling point (rapid iteration, heterogeneous data, natural fit for object graphs) and the trap (nothing stops you writing inconsistent shapes, so schema discipline moves into your application and validators).

Embed versus reference

Embedding nests related data inside the parent document. A blog post document can carry its recent comments as an array right inside it. One read returns the whole thing, no join, and the data that is read together is stored together, which is exactly what you want on a hot read path. Referencing stores an id pointer and fetches the related entity separately, the way a foreign key works, requiring a second lookup (an application-side join, since most document stores do not join efficiently).

The decision rule is driven by three questions. First, is the data read together? If you almost always render the post and its comments on the same page, embed the recent ones. Second, is the related entity large or independently accessed? An author appears on many posts; embedding a full copy into every post duplicates data and means updating the author's name touches thousands of documents. Reference the author. Third, how big and how unbounded is it? This is the killer.

Check yourself
A blog post document embeds its full comments array so one read returns everything. The post goes viral and comments never stop arriving. What eventually happens in MongoDB?

Document size limits. MongoDB caps a single document at 16MB. A post with an unbounded, ever-growing comments array will eventually hit that ceiling and the write fails. So the real pattern is hybrid: embed a bounded, frequently-read subset (the latest 20 comments, denormalized author name and avatar for display) and reference the unbounded remainder (the full comment history lives in its own collection, keyed by post id). This gives you a fast first render and a scalable long tail.

Interview nuance: The question that separates juniors from seniors is transactions. Atomicity is guaranteed per document. A single document update (including nested fields and arrays) is atomic, all-or-nothing. Multi-document transactions exist in modern MongoDB but are the exception, cost more, and were not available for years. So the idiomatic move is to model an operation that must be atomic as a single document. If updating a post and its comment count must happen together, put the count inside the post document and increment it in the same write. Reaching for multi-document transactions to patch a bad model is the wrong turn.

Indexing. You can index nested fields (author.id) and array elements (multikey indexes on tags). Plan indexes to your queries just as in SQL; an unindexed query on millions of documents is a full collection scan. And because there is no engine-enforced schema, plan schema versioning: stamp documents with a schemaVersion, migrate lazily on read or with a background job, and let your app handle multiple shapes during the transition.

Schema
posts
  • _id
  • title
  • body
  • author.id
  • author.namedenormalized copy
  • author.avatardenormalized copy
  • recentCommentsembedded, bounded x20
  • commentCountembedded counter
comments
  • _id
  • postId
  • authorId
  • body
  • createdAt
  • posts1-ncommentsfull unbounded history, referenced by postId
The hybrid layout: the post embeds the bounded read-together subset (latest 20 comments, the author's display fields, the counter it increments atomically in the same write) and references the unbounded remainder, so one read renders the page and no document grows toward the 16MB cap.

Recap: Model to the access pattern, embed bounded read-together data and reference large or unbounded entities, respect the 16MB document cap, and treat per-document atomicity as a design constraint rather than assuming relational multi-row transactions.

Check yourself

You are about to model a blog platform in a document store. Place each piece of the post page where the lesson's decision rule puts it.

The latest 20 comments with each author's display name
The full comment history
The comment count shown next to the title
The author's complete profile with bio and settings

Apply

Your turn

The task this lesson builds to.

Design the document model for a blog/CMS with posts, comments, and authors, deciding what to embed vs reference.

Think about

  1. What data is read together and should be embedded?
  2. When does referencing win despite requiring lookups?
  3. Why is atomicity per-document a constraint?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design the Firestore/MongoDB document model for Notion-style nested pages where a page contains blocks (text, images, tables, sub-pages) that can nest arbitrarily deep and a busy workspace page can have thousands of blocks. Explain your embed/reference split, how you avoid the document-size cliff, and how you keep block reordering fast.

Think about

  1. What breaks, twice, if the entire block tree is embedded in one page document?
  2. How does a fractional order key make reordering a single-block write?
  3. What does per-document atomicity give two users editing different blocks?