MongoDB lets documents in the same collection have different shapes. People sometimes treat that as a flaw. It is the exact feature that lets you change a schema without taking the database down, if you use it on purpose. The tool is a version field, and the discipline is migrating lazily instead of all at once. Here is the pattern.
Stamp each document with its version
Add a schemaVersion field. New writes carry the current version; existing documents keep whatever version they were written with:
// version 2 document
{ _id: 123, schemaVersion: 2, email: "sam@example.com", fullName: "Sam Rivers" }
// a version 1 document still in the collection
{ _id: 99, schemaVersion: 1, email: "lee@example.com", name: "Lee" }
Now your application knows, per document, which shape it is looking at. Version 1 has name; version 2 split it into fullName. The collection holds both, and that is fine, because the version field removes the guesswork.
Migrate on read: transform when you touch it
The lightest migration moves a document forward the next time the application reads it. Read, upgrade in memory, write the new shape back:
function toV2(doc) {
if (doc.schemaVersion === 2) return doc
return { ...doc, schemaVersion: 2, fullName: doc.name, name: undefined }
}
Documents that get read often migrate quickly; cold ones migrate whenever they are next touched. There is no migration window at all, the collection converts itself under normal traffic. The trade is that your read path carries the upgrade logic for a while, and some documents stay on the old version for as long as nobody reads them.
Migrate in the background: a slow, batched job
For the cold documents, or when you want to retire the old version entirely, run a background job that updates in batches and never locks the collection:
db.users.updateMany(
{ schemaVersion: 1 },
[{ $set: { schemaVersion: 2, fullName: "$name" } },
{ $unset: "name" }]
)
Run it in bounded chunks during quiet hours, watch the count of remaining schemaVersion: 1 documents fall, and stop when it hits zero. Because old and new coexist the whole time, the app keeps serving throughout.
Why not just rewrite everything at once
Because a big-bang migration has to rewrite every document in one window and be perfect on the first attempt, and it never is. (This is the schema equivalent of "we will fix it in prod," except the versioned version actually works.) One malformed document halfway through, and you are rolling back a half-migrated collection at the worst possible time. Versioning turns that cliff into a ramp.
Watch the migration finish
The one risk with a lazy migration is losing track of it: months later, a slice of documents is still on version 1, and a query written for version 2 quietly skips them. That is schema drift with a version number on it. The fix is to actually watch the version distribution until the old shape is gone, which is a schema-shape question. The whole approach builds on modeling the schema well in the first place, covered in MongoDB schema design and how to create a schema.
mongoui's Schema doctor samples the collection and shows the real shapes present, so a lingering pocket of old-version documents shows up instead of hiding until a query trips on it. It runs on your machine, so your documents never leave your network. Stamp the version, migrate lazily, and confirm the old shape is actually gone before you delete the upgrade code.
