Files

db-migrations-flyway-liquibase

Companion code for Flyway vs Liquibase for Spring Boot 4: Migrations, Rollbacks and Baselines — every claim in that post traces to a test in this module and a transcript in docs/output/.

Versions

Component Version
Spring Boot 4.1.1
Flyway (via spring-boot-starter-flyway) 12.4.0
Liquibase (via spring-boot-starter-liquibase) 5.0.3
Database H2 2.4.240 (file-based, AUTO_SERVER=TRUE)
JDK 25

Quickstart

mvn -DskipTests package
./scripts/run.sh                # default profile: Flyway only, clean startup
./scripts/run.sh both-naive     # both enabled, no baseline config — fails to start on purpose
./scripts/run.sh both-fixed     # both enabled, Flyway baselined at version 0 — starts cleanly
mvn test                        # regenerates every transcript in docs/output/

With the app running, hit the diagnostics endpoint to see both tools' bookkeeping tables live:

curl -s localhost:8080/diag/migrations | jq .

Scenarios (Spring profiles)

Profile What it demonstrates Config
(default) Flyway-only startup against a fresh database spring.flyway.enabled=true, spring.liquibase.enabled=false
both-naive Enabling both starters with no other configuration — fails on startup see chapter 14
both-fixed Both enabled, Flyway told to baseline at version 0 — coexists correctly spring.flyway.baseline-on-migrate=true, spring.flyway.baseline-version=0

Endpoints

Endpoint Purpose
GET /diag/migrations Plain-JDBC dump of both tools' tracking tables and the live table list — delete before shipping (see chapter 15)
GET /actuator/flyway, GET /actuator/liquibase Boot's own actuator endpoints, exposed in application.yml

Documentation

  1. The problem and the mental model
  2. Anatomy of a migration run
  3. Checksum validation
  4. Out-of-order migrations
  5. Repeatable migrations
  6. Baselining an existing database
  7. Why there is no undo
  8. Concurrent startup and locking
  9. Liquibase: anatomy of an update
  10. Rollback: auto-generated vs explicit
  11. Liquibase locking
  12. The OSS license service
  13. The FSL license change
  14. Running both at once
  15. Production checklist

Captured output

Every number quoted in the post and in the chapters above comes from a committed transcript in docs/output/, regenerated by mvn test via the Transcript helper — the tests assert the same numbers they print, so a transcript going stale fails the build:

File Test
01-flyway-happy-path.txt FlywayHappyPathTest
02-flyway-checksum-mismatch.txt FlywayChecksumMismatchTest
03-flyway-out-of-order.txt FlywayOutOfOrderTest
04-flyway-repeatable.txt FlywayRepeatableTest
05-flyway-baseline.txt FlywayBaselineTest
06-flyway-undo-teams-required.txt FlywayUndoTest
07-flyway-proprietary-stub-commands.txt FlywayCommunityCommandSurfaceTest
08-flyway-concurrent-lock.txt FlywayConcurrentMigrateTest
09-liquibase-happy-path.txt LiquibaseHappyPathTest
10-liquibase-rollback-auto.txt LiquibaseRollbackAutoTest
11-liquibase-rollback-no-inverse.txt LiquibaseRollbackFailTest
12-liquibase-rollback-explicit.txt LiquibaseRollbackExplicitTest
13-liquibase-lock-contention.txt LiquibaseConcurrentUpdateTest
14-liquibase-oss-license-service.txt LiquibaseLicenseServiceTest
15-both-together-same-datasource.txt BothTogetherTest

One file is the exception: 16-liquibase-lock-defaults-javap.txt isn't produced by mvn test — it's the trimmed javap -p -c -constants output confirming Liquibase's default lock-poll and lock-wait settings straight from liquibase-core's bytecode (see chapter 11), captured by hand since there's no JVM assertion that reads a compiled class's own constant pool.

Findings worth the trip

  • Flyway Community's undo, diff, check, deploy, generate, model, prepare and auth commands all compile fine and throw FlywayRedgateEditionRequiredException only at runtime — there is no working rollback in Flyway Community at all (chapter 7).
  • H2 case-folds unquoted identifiers to uppercase, so an unquoted query against flyway_schema_history (created and queried by Flyway using quoted lowercase) silently finds nothing — a real bug this module's own diagnostics endpoint had and fixed (chapter 2).
  • Liquibase's default lock-poll rate is 10 seconds, confirmed by decompiling GlobalConfiguration's bytecode — a losing instance can wait up to ten seconds for sub-second work, unlike Flyway's near-instant row-lock release (chapter 11).
  • A genuine Liquibase 5.0.3 defect: reusing the same simple changelog filename for two logically different changelogs causes a phantom "successful" run where the changeset never actually executes (chapter 11).
  • Liquibase Community 5.0 shipped under the Functional Source License, not Apache 2.0 — a real, ongoing compliance question for projects like Apache Fineract (ASF LEGAL-721) and Keycloak (GitHub #43391) (chapter 13).
  • Enabling both Flyway and Liquibase against one database fails to start by default, and fixing it does not integrate them — it just gets you two independent bookkeepers, each blind to the other's tables (chapter 14).