MongoDB / Basic: document foundations / Lesson 2
Modeling embedded and referenced data
Choose document boundaries from access patterns and bounded growth. Explain embedding, references and duplication without treating flexible schema as no design.
On this page
Lesson goals
- Choose an aggregate boundary for atomic business updates.
- Identify unbounded arrays and duplicated-fact ownership.
Design from operations and invariants
A document boundary should fit facts read or changed together. A shipment with a small fixed destination snapshot can embed that address, while an indefinitely growing event history usually deserves separate documents. MongoDB does not require relational joins for every shape, but references remain valid when lifecycle, reuse or growth make embedding unsuitable. List the dominant reads, writes, retention policy and consistency rules before choosing a structure. Flexible fields reduce migration friction only when their meanings and valid versions are still documented.
Embed a deliberate historical snapshot
This local document records the address used when booking 101 was accepted. It can remain unchanged if the customer later edits their current address. That is intentional historical duplication rather than a failed synchronization job. The local access prints Aqaba without consulting a customer collection. If the report instead needs the customer’s current address, name that requirement separately and use the authoritative reference. A field called address is insufficient documentation when different consumers mean historical and current facts.
const booking = {
_id: 101, customerId: 7,
deliveryAddress: { city: 'Aqaba', street: 'Quay Road' },
lines: [{ sku: 'PEN', qty: 2 }, { sku: 'PAD', qty: 1 }]
};
print(booking.deliveryAddress.city);
Aqaba
The embedded address is the booking snapshot, while customerId can identify separately managed customer data.
Bound arrays and document size
MongoDB’s maximum BSON document size is 16 MiB for ordinary documents. An ever-growing scans array can reach that limit and also turn independent events into contention on one hot document. Keep a bounded recent summary only when its limit and update policy are clear; store complete history separately with an indexed shipment reference. Document-size limits are not performance targets: a document far below the limit may still be expensive to rewrite, transfer and cache repeatedly under a high-rate workload.
Keep history as separately queryable events
The training pattern retrieves scan events by shipmentId, ordered by event time and _id for ties. The query says nothing about whether an event is authorized merely because its reference matches: tenant scope belongs in the filter when the application model requires it. An appropriate compound index is considered later. The expected logical result is that only this shipment’s matching events appear in the specified order; exact documents require a synthetic fixture and are not invented as observed production output.
db.getSiblingDB('quay_lab').scanEvents.find(
{ shipmentId: 101 },
{ _id: 1, occurredAt: 1, state: 1 }
).sort({ occurredAt: 1, _id: 1 }).limit(20);
A separate event collection avoids requiring the entire historical array in every shipment document read.
Name who updates duplicated facts
Duplication can serve historical snapshots or faster reads, but a copied current fact needs an owner and propagation policy. Decide whether updates are synchronous, eventually consistent or rebuilt from authoritative data. Embedding gives a single-document atomicity boundary; multiple referenced documents do not automatically update atomically together. A transaction can coordinate supported multi-document work, but it does not make an unbounded model efficient. Model invariants first, then choose transactions, an outbox or reconciliation according to the acceptable consistency delay and failure behavior.
Common errors and fixes
An events array grows without a bound
- Why it happens
- Historical one-to-many growth was embedded inside one parent.
- How to fix it
- Move full history to referenced event documents and define a bounded summary.
- Prevent it next time
- Estimate growth and access cost before choosing embedding.
A historical address changes after customer edits
- Why it happens
- A current reference was used where an immutable booking snapshot was required.
- How to fix it
- Store the accepted address snapshot with explicit business meaning.
- Prevent it next time
- Distinguish current and historical fields in the schema contract.
Copied current values disagree across documents
- Why it happens
- No authoritative owner or propagation/reconciliation rule was defined.
- How to fix it
- Choose the source of truth and implement a reviewed update policy.
- Prevent it next time
- Document tolerated staleness and repair evidence for every duplicate current fact.
Check your understanding
1. Should a shipment embed its latest three status summaries and its entire lifetime event history in one unbounded array?
Show worked solution
A latest-three summary can be appropriate when its bounded update rule is clear, but the complete history should not become an unlimited parent array. Store event documents with shipmentId and a useful time index. Define which representation is authoritative and how the summary is rebuilt after failure so bounded denormalization does not become unexplained inconsistency.
2. A customer changes city after placing a booking. Which city should an invoice show?
Show worked solution
Answer the business question first: an invoice usually needs the accepted billing or delivery snapshot rather than whatever current profile now contains. Store and label that snapshot deliberately. A current-contact report may legitimately follow the customer reference instead. The database model should support both meanings without pretending they are one automatically synchronized field.
Key takeaways
- Embed bounded facts changed together; reference independent or growing histories.
- Every duplicated current fact needs ownership and a consistency policy.