# db-migrations-flyway-liquibase Companion code for **[Flyway vs Liquibase for Spring Boot 4: Migrations, Rollbacks and Baselines](https://ankurm.com/)** — every claim in that post traces to a test in this module and a transcript in [`docs/output/`](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 ```bash 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: ```bash 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](docs/14-running-both-at-once.md) | | `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](docs/15-production-checklist.md)) | | `GET /actuator/flyway`, `GET /actuator/liquibase` | Boot's own actuator endpoints, exposed in [`application.yml`](src/main/resources/application.yml) | ## Documentation 1. [The problem and the mental model](docs/01-the-problem-and-mental-model.md) 2. [Anatomy of a migration run](docs/02-anatomy-of-a-migration-run.md) 3. [Checksum validation](docs/03-checksum-validation.md) 4. [Out-of-order migrations](docs/04-out-of-order-migrations.md) 5. [Repeatable migrations](docs/05-repeatable-migrations.md) 6. [Baselining an existing database](docs/06-baselining-an-existing-database.md) 7. [Why there is no undo](docs/07-why-there-is-no-undo.md) 8. [Concurrent startup and locking](docs/08-concurrent-startup-and-locking.md) 9. [Liquibase: anatomy of an update](docs/09-liquibase-anatomy-of-an-update.md) 10. [Rollback: auto-generated vs explicit](docs/10-rollback-auto-generated-vs-explicit.md) 11. [Liquibase locking](docs/11-liquibase-locking.md) 12. [The OSS license service](docs/12-the-oss-license-service.md) 13. [The FSL license change](docs/13-the-fsl-license-change.md) 14. [Running both at once](docs/14-running-both-at-once.md) 15. [Production checklist](docs/15-production-checklist.md) ## Captured output Every number quoted in the post and in the chapters above comes from a committed transcript in [`docs/output/`](docs/output), regenerated by `mvn test` via the [`Transcript`](src/test/java/com/ankurm/dbmigrations/Transcript.java) helper — the tests assert the same numbers they print, so a transcript going stale fails the build: | File | Test | |---|---| | [`01-flyway-happy-path.txt`](docs/output/01-flyway-happy-path.txt) | [`FlywayHappyPathTest`](src/test/java/com/ankurm/dbmigrations/flyway/FlywayHappyPathTest.java) | | [`02-flyway-checksum-mismatch.txt`](docs/output/02-flyway-checksum-mismatch.txt) | [`FlywayChecksumMismatchTest`](src/test/java/com/ankurm/dbmigrations/flyway/FlywayChecksumMismatchTest.java) | | [`03-flyway-out-of-order.txt`](docs/output/03-flyway-out-of-order.txt) | [`FlywayOutOfOrderTest`](src/test/java/com/ankurm/dbmigrations/flyway/FlywayOutOfOrderTest.java) | | [`04-flyway-repeatable.txt`](docs/output/04-flyway-repeatable.txt) | [`FlywayRepeatableTest`](src/test/java/com/ankurm/dbmigrations/flyway/FlywayRepeatableTest.java) | | [`05-flyway-baseline.txt`](docs/output/05-flyway-baseline.txt) | [`FlywayBaselineTest`](src/test/java/com/ankurm/dbmigrations/flyway/FlywayBaselineTest.java) | | [`06-flyway-undo-teams-required.txt`](docs/output/06-flyway-undo-teams-required.txt) | [`FlywayUndoTest`](src/test/java/com/ankurm/dbmigrations/flyway/FlywayUndoTest.java) | | [`07-flyway-proprietary-stub-commands.txt`](docs/output/07-flyway-proprietary-stub-commands.txt) | [`FlywayCommunityCommandSurfaceTest`](src/test/java/com/ankurm/dbmigrations/flyway/FlywayCommunityCommandSurfaceTest.java) | | [`08-flyway-concurrent-lock.txt`](docs/output/08-flyway-concurrent-lock.txt) | [`FlywayConcurrentMigrateTest`](src/test/java/com/ankurm/dbmigrations/flyway/FlywayConcurrentMigrateTest.java) | | [`09-liquibase-happy-path.txt`](docs/output/09-liquibase-happy-path.txt) | [`LiquibaseHappyPathTest`](src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseHappyPathTest.java) | | [`10-liquibase-rollback-auto.txt`](docs/output/10-liquibase-rollback-auto.txt) | [`LiquibaseRollbackAutoTest`](src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseRollbackAutoTest.java) | | [`11-liquibase-rollback-no-inverse.txt`](docs/output/11-liquibase-rollback-no-inverse.txt) | [`LiquibaseRollbackFailTest`](src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseRollbackFailTest.java) | | [`12-liquibase-rollback-explicit.txt`](docs/output/12-liquibase-rollback-explicit.txt) | [`LiquibaseRollbackExplicitTest`](src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseRollbackExplicitTest.java) | | [`13-liquibase-lock-contention.txt`](docs/output/13-liquibase-lock-contention.txt) | [`LiquibaseConcurrentUpdateTest`](src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseConcurrentUpdateTest.java) | | [`14-liquibase-oss-license-service.txt`](docs/output/14-liquibase-oss-license-service.txt) | [`LiquibaseLicenseServiceTest`](src/test/java/com/ankurm/dbmigrations/liquibase/LiquibaseLicenseServiceTest.java) | | [`15-both-together-same-datasource.txt`](docs/output/15-both-together-same-datasource.txt) | [`BothTogetherTest`](src/test/java/com/ankurm/dbmigrations/BothTogetherTest.java) | ## 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](docs/07-why-there-is-no-undo.md)). - 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](docs/02-anatomy-of-a-migration-run.md)). - 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](docs/11-liquibase-locking.md)). - 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](docs/11-liquibase-locking.md)). - 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](docs/13-the-fsl-license-change.md)). - 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](docs/14-running-both-at-once.md)).