# 14. Running both at once [← 13. The FSL license change](13-the-fsl-license-change.md) · [Index](../README.md) · Next: [15. Production checklist →](15-production-checklist.md) 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`](../src/test/java/com/ankurm/dbmigrations/BothTogetherTest.java) 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`](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](06-baselining-an-existing-database.md) 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`](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`](../src/main/resources/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](https://docs.spring.io/spring-boot/reference/data/sql.html) (`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.