Polymorphic Documents and Schema Evolution
One collection can hold related variants
A polymorphic collection stores multiple document shapes that share a common identity or query path. This can be useful for products with different attributes, payment methods, notifications, or content types.
// Physical product
{
_id: ObjectId("65a000000000000000000070"),
type: "physical",
name: "Keyboard",
price: 4599,
shippingWeightGrams: 850
}
// Digital product
{
_id: ObjectId("65a000000000000000000071"),
type: "digital",
name: "MongoDB Course",
price: 1299,
downloadUrl: "https://example.invalid/download/asset-71"
}
The type field identifies the variant. Shared queries can target fields such as name and price; type-specific queries can filter by type.
When polymorphism is appropriate
Use it when variants share enough behavior and query patterns that one collection is manageable. Prefer separate collections when variants have fundamentally different security requirements, retention, write patterns, validation rules, or indexes.
Schema evolution without breaking old documents
Applications evolve. A field may be added, renamed, or replaced. Avoid assuming every existing document changes immediately when a new application version deploys.
A safe migration often follows an expand-migrate-contract sequence:
- Expand: make the application tolerate both old and new shapes; begin writing the new field.
- Migrate: backfill old documents in controlled batches.
- Verify: measure how many documents still use the old shape and validate data.
- Contract: remove old-field support only after the migration is complete and all deployed readers are compatible.
For example, migrate fullName to name without assuming the backfill has finished:
db.users.updateMany(
{ name: { $exists: false }, fullName: { $type: "string" } },
[
{
$set: {
name: "$fullName",
schemaVersion: 2
}
}
]
);
For a large production collection, prefer batched migration and monitor replication lag, write load, errors, and completion. A single updateMany can be suitable for small collections but may be too disruptive for very large datasets.
Version fields
A field such as schemaVersion can help migrations and debugging, but it is not mandatory for every collection. Use it when the application genuinely needs to distinguish incompatible shapes or migration states. Do not add a version number without a policy for what each version means.
- Polymorphic collection:
A collection containing related document variants. - Expand-migrate-contract:
A staged rollout that keeps old and new application versions compatible during migration. - schemaVersion:
An optional field identifying the schema shape or migration version.
Schema migrations must account for old application instances, retries, partial progress, and rollback. Make readers tolerant before changing the stored shape.
// All products use common fields
db.products.find(
{ active: true },
{ name: 1, price: 1, type: 1 }
);
// Only physical products have shipping weight
db.products.find({
type: "physical",
shippingWeightGrams: { $gt: 1000 }
});- Use a discriminator when related document variants share common query behavior.
- Use staged migrations for large or continuously accessed collections.
- Add schema versioning only when it serves a clear compatibility or migration need.
Why should a schema migration often use an expand-migrate-contract sequence?