Skillversitas

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

SignalEmbeddedSeparate collection + reference
Access patternalways with the parentqueried or updated on its own
Cardinalitybounded (a handful to a few hundred)unbounded (thousands+)
Growthchildren grow slowly, if at allchildren grow independently
Writesparent writes dominatechildren 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.