Files
asmhatreandClaude Sonnet 5 3908331431 Add db-migrations-flyway-liquibase: Flyway vs Liquibase migrations, rollbacks and baselines on Spring Boot 4.1
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
2026-09-15 07:08:57 +00:00

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.

one DataSource 1. Liquibase runs first creates PRODUCT, DATABASECHANGELOG* 2. Flyway runs second sees unknown tables, no history → fails Flyway's safety check that protected you in chapter 6 (a real database it didn't build yet) fires here too — it can't tell "existing production schema" apart from "Liquibase got here first". Same check, same message, different cause.

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.