Two feature branches each add a migration numbered V37 and both merge in the same week. What problems does that cause, and how do teams keep migration ordering sane when many people work in parallel?
answer
- two branches, one V37
- duplicate version vs out-of-order arrival
- timestamp versions / renumber at merge
- make migrations commute
- CI: build from empty + replay on snapshot
basics
~20 sDuplicate versions either abort the run or, once renumbered, apply in an order nobody tested — and databases already past V37 skip the late arrival. Fixes: allocate versions at merge time (or use timestamp versions), keep migrations independent of each other, and rebuild the schema from scratch in CI on every merge.
solid answer
~60 sTwo problems compound. First, **collision**: two files claim version 37, and Flyway refuses to run with a duplicate version. Second, and subtler, **out-of-order arrival**: whichever branch merges second gets renumbered to V38, but a database that already ran the first V37 and then V38 has a different applied sequence than a database built from scratch, and any developer database sitting at V38 will simply never see a migration renumbered to V37 unless out-of-order mode is enabled. Mitigations, roughly in order of value: - **Allocate the version late** — timestamp-based names (`V20260812103000__…`) make collisions almost impossible, or renumber at merge time as part of the pull-request checklist. - **Make migrations mutually independent** so that any interleaving is valid; migrations that assume a sibling ran first are the real defect. - **CI rebuilds an empty database from scratch on every merge** and also replays against a production-like snapshot; a bad ordering fails there, not in production. - Prefer many small additive migrations to one big one, so conflicts are rare and cheap.
code
text · 9 linesAlice's laptop CI fresh build
------------------ ------------------
V36 (trunk) V36
V37 add_orders_status V37 add_orders_status <- branch A
V38 add_orders_index V38 add_orders_index <- branch B
... but if branch B merged first and A was renumbered:
Alice has applied A@37 then B@38; CI applies B@37 then A@38.
Identical result only if A and B commute.go deeper
Recognise that two files with the same version collide and that the tool will refuse to run; the practical fix is renumbering before merge.
Distinguish the collision from out-of-order arrival, and explain why timestamp-based version names largely remove the collision.
Focus on commutativity — which migrations can safely interleave — plus out-of-order mode, CI replaying onto a production snapshot, and never renumbering an applied script.
Treat it as a branching-strategy question: how many parallel schema changes the team can sustain, whether migrations are owned per service, and what pipeline gates make ordering mistakes fail at pull-request time.
## Why the number matters A versioned migration tool applies pending scripts in version order and records what it applied. That works perfectly on a single linear history. Branching breaks the assumption: each branch extends the same linear sequence independently, so both authors reach for the next free number, and the trunk ends up with two claims on it. ## Failure one: duplicate versions Flyway treats a duplicate version as a hard error and refuses to run — annoying but safe, because it surfaces at build time. Liquibase, keyed on `id + author + filename`, will not report a duplicate at all; both changesets simply execute, in changelog-inclusion order. That is safer against collision but shifts the risk to the *master changelog* file, which becomes the merge conflict point instead. ## Failure two: out-of-order arrival Suppose branch A merges first as `V37`, branch B is renumbered to `V38`, and everything is fine. Now consider the opposite ordering in the wild: - Alice's laptop ran her own `V37` (branch A) days ago. - Branch B merges and its script keeps the number `V37` under a different name. - Alice pulls. Flyway sees a *new* script whose version is not greater than the highest applied version, and by default fails with an "out of order" error; with `outOfOrder=true` it applies it, but her database's applied sequence is now A-then-B while CI's fresh build is B-then-A. If the two migrations are genuinely independent, both orders yield the same schema and the divergence is harmless. If one depends on the other — B adds a foreign key to a table A created, or both alter the same column — the orders are not equivalent, and one of the two environments is silently wrong or the migration fails only in some environments. This asymmetry is what makes the problem a senior-level topic: the mechanical collision is easy, the semantic one is not. ## Fix one: allocate the version as late as possible The collision exists because two people picked the number at *branch start*. Two ways to move the decision to merge time: - **Timestamp versions.** `V20260812_1030__add_orders_status.sql`. Two developers colliding requires the same second. Ordering follows wall-clock authoring time, which is not the same as merge time, but it is deterministic and unique. This is the most common answer. - **Renumber on merge.** Keep sequential numbers but make it the merging author's job to move their file to the next free number and re-run migrations locally. Cheap in a small team, error-prone in a large one. A repository convention that puts each migration in its own file with a globally unique name (and a CI check that fails on duplicate versions) turns a production incident into a red pull request. ## Fix two: make ordering not matter The deeper fix is designing migrations so that any interleaving with concurrently developed migrations is valid: - Each migration touches its own objects and does not depend on another in-flight migration. - Additive first: add a table, add a nullable column, add an index. Additive changes commute with almost anything. - If two changes genuinely must be sequenced, they belong in the *same* migration or the *same* branch, merged together. When migrations commute, out-of-order application is a non-event and you can safely enable out-of-order mode for developer environments. ## Fix three: prove it in CI Two pipelines catch the two shapes of failure: 1. **Build from empty.** Every merge creates a database from nothing and replays the whole migration history. This catches duplicates, broken SQL, and dependency-order bugs in the canonical ordering. 2. **Replay onto a snapshot.** Restore a recent production-shaped dump (schema, sanitised data) and apply only the pending migrations. This catches the case that only breaks against real prior state — a `NOT NULL` added to a column that has nulls in production, a unique index that real data violates, a migration that takes 40 minutes on 200 million rows. Running both on every pull request, not just on the release branch, is what makes parallel development safe. ## Liquibase-specific note Because Liquibase includes changelog files from a master changelog, the merge conflict lands in that master file rather than in the numbering. The convention that avoids daily pain is one changelog file per release or per change, `include`d in date order, with `logicalFilePath` set so a later file move does not change a changeset's identity and cause it to re-run. ## Never do this Renumbering a migration that has *already been applied* somewhere shared. For Flyway the version is part of the identity, so changing it makes the script look pending again and it re-executes; for Liquibase, changing the file path or id has the same effect unless `logicalFilePath` pins it.
- When is it safe to enable out-of-order migration application?When migrations are mutually independent, so any interleaving produces the same schema — typically additive changes touching disjoint objects. It is reasonable for developer and CI databases, where the cost of a wrong guess is a rebuild. On production it is worth more caution, because an out-of-order script has by definition never been exercised in that exact sequence.
- Why do timestamp-based versions not fully solve ordering?They guarantee uniqueness, so collisions disappear, but they order by authoring time rather than merge time. A migration written on Monday and merged on Friday sorts before one written and merged on Wednesday, so a database that already ran the Wednesday script receives the Monday one out of order. Uniqueness is solved; commutativity still has to be designed for.
saying these in an interview costs you the question
- Renumbering a migration that has already been applied to a shared database — it re-executes under its new identity
- Assuming Liquibase has no ordering problem because it keys on id/author/filename; the conflict just moves to the master changelog
- Believing a green CI run on an empty database proves the migration is safe against production data
- Resolving a duplicate-version conflict by merging two unrelated migrations into one file