Lesson 03 · The pragmatist

Document: the object you meant to store

Your application thinks in objects: a blog post has comments, an order has line items, a Cyberdesk identity has its permission grants. Lesson 1's model shreds each object across tables on write and re-joins it on read. The document model's proposal: skip the shredding: store the tree you already have. A document is a self-contained JSON/BSON value with nesting and arrays, addressed by id, but: unlike Lesson 2's opaque blobs: the database can see inside: it indexes fields, filters on them, updates them in place. Document = key-value's shape + relational's queryability, aimed at one document at a time.

The happy path: one object, one read

Load a blog post two ways

The page needs: post + author name + comments. Compare the relational assembly with the document fetch.

:
queries
:
round trips
:
joins
:

Locality is the document model's entire physics: data read together is stored together, so one seek retrieves the working set. When your access pattern is "load the whole aggregate, render it, maybe update a field," documents are simply the correct shape. The model also removes a ceremony: no ALTER TABLE: new documents can carry new fields today. But both gifts invoice you later, and the invoices are the two labs below.

Invoice #1: the day the copies disagree

Rename an author who's been denormalized everywhere

To render post lists without a second lookup, each of the 214 posts embeds a copy of author.name. The author gets married and changes name. Run the rename.

{ _id: "post:1", title: "...", author: { id: "u7", name: "Mia Aalto" }, ... } { _id: "post:2", title: "...", author: { id: "u7", name: "Mia Aalto" }, ... } ... 212 more posts with embedded copies ...
documents to update
:
updated so far
:
crash risk window
:

Recognize the disease? It's Lesson 1's update anomaly, returned with interest: you chose to store one fact 214 times, so one change is 214 writes, and any partial failure leaves the database disagreeing with itself. The document world's answer is not "never embed" but embed by the rules: embed data that is owned by the parent, read with it, and bounded in size (comments in a post, line items in an order); reference by id anything shared, frequently changing, or unbounded (authors, products, anything with its own lifecycle). The mantra from the MongoDB documentation era: "data that is accessed together should be stored together": with the silent second clause: data that changes independently should live independently.

Invoice #2: the schema didn't disappear: it moved

Read three generations of the same collection

Three years of "flexible schema" writes are in the users collection. The notification service does user.contact.email.toLowerCase(). Run it against each generation.

gen-2024: { _id:"u1", email: "mia@x.fi" } gen-2025: { _id:"u2", contact: { email: "rui@x.pt" } } gen-2026: { _id:"u3", contact: { emails: ["sam@x.se", "sam@work.se"] } }
:

"Schemaless" was always a half-truth: the schema exists: it just moved from the database (one enforced place, checked on write) into every code path that reads the data (checked never, discovered in production). This is schema-on-read, and it's a real trade, not a trick: for fast-evolving products it genuinely removes migration friction: but it obligates you to write readers that tolerate every historical shape, forever, or to run backfill jobs that are migrations by another name. Mature document stores split the difference: MongoDB offers optional JSON-schema validation per collection: turning back, knob by knob, into Lesson 1.

Species notes: MongoDB: the reference document store: rich queries, secondary indexes, aggregation pipelines, multi-document ACID transactions since 4.0 (with a performance tax that keeps the single-document design discipline alive). CouchDB: document store organized around replication and offline-first sync (its conflict handling is the replication course's multi-leader lesson in production). Firestore: documents + real-time subscriptions for app backends. DynamoDB: marketed key-value but with maps/lists inside items and secondary indexes, it's substantially a document store with Lesson 4's key discipline. PostgreSQL's JSONB: the incumbent absorbing the challenger: document columns, indexed with GIN, inside ACID tables; for mixed workloads, often the best of both.

Check yourself

1. An e-commerce order with its line items: embed or reference? A product's reviews (potentially 100k of them): embed or reference?

Line items are the textbook embed: an order's items are part of the order as a historical fact: copying the price at purchase time isn't an anomaly, it's correctness. Reviews violate two embed rules at once: unbounded (documents have size limits: 16MB in MongoDB: and "the document grows forever" ruins locality) and independently accessed (review moderation shouldn't fetch products). The rules from invoice #1 decide almost every real case.

2. "We chose a document store so we don't have to design a schema." What's the accurate correction?

The embed-vs-reference decisions of invoice #1 are schema design: with higher stakes, since access patterns are baked into document shape. And invoice #2 shows where enforcement went: nowhere, until a reader crashes. Flexibility is real and valuable (especially early-stage); freedom from design is not on offer anywhere in this course.

3. In most document stores, updating a single document (however deeply nested) is atomic, while multi-document transactions are extra-cost or absent. How does this shape good data modeling?

This is the deep reason "aggregate design" dominates document-store literature (and DDD before it): the unit of atomicity should equal the unit of consistency. An order whose total must match its items → one document, one atomic update. Cross-document invariants → either restructure, or accept saga-style eventual consistency (distributed systems course, lesson 4). The model rewards getting the boundary right and punishes getting it wrong: in the same currency.