Companion code for the Flyway vs Liquibase article: checksum validation, out-of-order and repeatable migrations, baselining an existing schema, Flyway Community's undo/diff/deploy stubs, concurrent-startup locking for both tools, Liquibase changeset identity and rollback (auto-generated vs explicit), a verified Liquibase 5.0.3 filename-caching defect, the new OSS license service, the FSL license change, and running both tools against one database. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Q6XdRjtsp4862EM44T7i9a
6.0 KiB
14. Running both at once
← 13. The FSL license change · Index · Next: 15. Production checklist →
Every "Flyway or Liquibase" thread eventually gets a comment asking whether you can just run
both — maybe one team owns a legacy Liquibase changelog and another is standardizing new services
on Flyway, and for a while both point at the same database.
BothTogetherTest answers it
directly: enable both starters against the same DataSource, with nothing else configured, and
see what actually happens on startup.
The naive answer: it refuses to start
context failed to start: FlywayException: Found non-empty schema(s) "PUBLIC" but no schema history table. Use baseline() or set baselineOnMigrate to true to initialize the schema history table.
(from docs/output/15-both-together-same-datasource.txt)
In this configuration, Spring Boot wires Liquibase's SpringLiquibase bean before Flyway's
FlywayMigrationInitializer runs — an artifact of bean registration order here, not a documented
contract you should rely on. By the time Flyway gets its turn, Liquibase has already created a
product table and its own DATABASECHANGELOG/DATABASECHANGELOGLOCK tables. Flyway looks at a
non-empty schema with no flyway_schema_history table and does exactly what chapter
6 said it would do the first time it meets an existing
database: it refuses to guess, and startup fails.
The fix: tell Flyway nothing has run yet
Turning on baseline-on-migrate alone isn't enough. Its default baseline-version is 1, which
tells Flyway "pretend V1 already ran" — correct only if the existing schema already matches what
V1 would have built. Here it doesn't: Liquibase's changelog built an unrelated product table,
not customer. Setting baseline-version=0 instead tells Flyway "nothing of mine has run, apply
my whole migration set from scratch", which is what actually needs to happen alongside whatever
Liquibase already put there:
TABLE_NAME
---------------------
CUSTOMER
DATABASECHANGELOG
DATABASECHANGELOGLOCK
PRODUCT
flyway_schema_history
(5 rows)
-- flyway_schema_history — baseline row at 0, then V1/V2 ran for real --
version | description | type | success
--------+-------------------------------------------+----------+--------
NULL | << Flyway Schema History table created >> | TABLE | true
0 | << Flyway Baseline >> | BASELINE | true
1 | create customer | SQL | true
2 | seed customer | SQL | true
(4 rows)
-- databasechangelog — Liquibase's own bookkeeping, untouched by Flyway --
ID | AUTHOR | EXECTYPE
-----------------+--------+---------
1-create-product | ankurm | EXECUTED
2-seed-product | ankurm | EXECUTED
(2 rows)
(from docs/output/15-both-together-same-datasource.txt)
Both tools end up with their own tracking table, each accounting only for what it built:
customer under Flyway, product under Liquibase, and neither one aware the other exists. That's
the real shape of "running both" — not integration, just two independent bookkeepers sharing a
schema, each blind to the other's tables. You can reproduce this exact split locally:
application.yml's both-naive profile fails to start
the way the transcript above shows, and both-fixed starts cleanly with the baseline settings
already applied — run them with ./scripts/run.sh both-naive and ./scripts/run.sh both-fixed.
Going deeper
- This module never asked Flyway or Liquibase to migrate the same table — the moment two tools try to own the same object, you're outside anything either one tests for, and it becomes your migration author's job to keep them apart by convention (naming, schemas, or ownership documented somewhere a new teammate will actually read).
- The bean-ordering behavior that makes Liquibase run first here is not a documented contract — treat it as incidental, not as something to design a real system around. A real migration from one tool to the other should have a clean cutover point, not a permanent "both, forever" steady state.
- Spring Boot's own note on combining Flyway and Liquibase (
rel="nofollow") — Boot's docs confirm both can be enabled together but don't document an execution order between them, which matches what this test observed rather than what any spec promises.