Embedded Documents
Embedded documents model the "has many, tightly coupled" relationship: a list of typed subdocuments stored inside the parent document. The children are written, read, and deleted as part of the parent - one round trip, atomic by construction, no join. In MongoDB terms this is the array-of-subdocuments pattern, and it is the right shape whenever the children are always needed together with the parent, are bounded in number, and are not queried independently.
The Pattern
from mongoengine import (Document, StringField, IntField,
EmbeddedDocument, EmbeddedDocumentField, ListField)
class Address(EmbeddedDocument):
street = StringField(required=True)
city = StringField()
zip_code = StringField()
class User(Document):
name = StringField(required=True)
addresses = ListField(EmbeddedDocumentField(Address), default=list)
meta = {"indexes": ["name"]}
Three rules make embeddings clean. First, the child class extends EmbeddedDocument, never Document - embedded documents have no _id of their own and cannot be saved or queried directly. Second, the parent declares ListField(EmbeddedDocumentField(Child)) (or a single EmbeddedDocumentField for a one-off child). Third, validation of the child fields runs when the parent validates, so a malformed address fails the whole save().
Using Them
user = User(name="ada")
user.addresses.append(Address(street="1 Main St", city="Springfield"))
user.addresses.append(Address(street="2 Oak Ave", city="Shelbyville"))
user.save() # one document, both addresses inside it
# reading: the children come back attached
fetched = User.objects.get(name="ada")
print(fetched.addresses[0].street) # '1 Main St'
print(len(fetched.addresses)) # 2
# modifying a child means modifying the parent, then saving
fetched.addresses[0].city = "Capital City"
fetched.addresses.pop() # remove an element
fetched.save()
The parent's save() rewrites the entire document, so a child change is a parent write: one operation, fully atomic, with no separate "save the child" step. That atomicity is the feature - an address list can never be half-updated while the user exists in another state.
Querying Into Embeddings
# dot-path equality: any embedded element matches
User.objects(addresses__city="Springfield")
# with operators on the embedded field
User.objects(addresses__zip_code__startswith="9")
# a specific element by index (MongoDB array indexing)
User.objects("addresses.0.city": "Springfield")
Dot-path filters reach into embedded documents and arrays: addresses__city="X" matches any user with an address in that city, exactly as a native MongoDB query would. The positional form ("addresses.0.city") targets a specific array slot. These queries can use indexes (the indexes chapter covers compound index entries for embedded fields).
Embedded vs Separate Collection
| Signal | Embedded | Separate collection + reference |
|---|---|---|
| Access pattern | always with the parent | queried or updated on its own |
| Cardinality | bounded (a handful to a few hundred) | unbounded (thousands+) |
| Growth | children grow slowly, if at all | children grow independently |
| Writes | parent writes dominate | children written frequently alone |
The hard constraint is the 16 MB document size limit: an embedding whose array can grow without bound will eventually cross it. The rule of thumb: embed when the children are a bounded, always-together part of the parent (addresses, tags-as-objects, order line items); reference when they have a life of their own (a user's posts, a product's reviews). The references chapter covers that side.
Summary
Model tightly coupled children as EmbeddedDocument classes in a ListField(EmbeddedDocumentField(...)) on the parent: they validate with the parent, save atomically with it, and are queried with dot-path filters. Keep the arrays bounded - the 16 MB document limit is real - and reach for a separate collection plus a reference the moment the children need independent queries, frequent standalone writes, or unbounded growth.