The Problem
Every team eventually hits the same failure mode: a new build goes out, the application starts, and it immediately breaks because a required schema change wasn’t applied first. Maybe someone forgot to run the SQL script. Maybe it ran against the wrong environment. Maybe it ran after the app already tried to query a column that doesn’t exist yet.
The fix isn’t “remind people to run the script.” The fix is removing the human step entirely — making schema changes a mandatory, ordered, tracked part of the deployment pipeline, so the application literally cannot start against a database that isn’t in the expected state.
There are two broad ways to get there: let the application enforce it at boot, or let the pipeline enforce it before deploy. The two dominant tools — Flyway and Liquibase — map cleanly onto this split, though Liquibase can actually do either.
Option 1: Flyway — Migrations Run at Application Startup
Flyway is the simpler of the two tools by design. You keep versioned SQL files (V1__create_users.sql, V2__add_email_index.sql, etc.) inside the application repo. When the Spring Boot app starts, Flyway’s auto-configuration wakes up before the rest of the context loads, checks a flyway_schema_history table to see which migrations have already run, and applies anything new — in strict numeric order. If a migration fails, the app refuses to finish starting.
Why this enforces the rule automatically: there is no separate step to forget. The migration is the startup sequence. No Jenkins stage, no manual script run, no possibility of the app and schema drifting out of sync, because they’re deployed as one atomic unit — the same JAR that contains the code also contains the migrations.
The tradeoff: every ECS task launch checks the history table before serving traffic, and rollback isn’t automatic — Flyway’s philosophy is “roll forward,” so undoing a bad migration means writing a new migration that reverses it, not invoking a built-in rollback command (that’s a paid Flyway Teams feature).
Option 2: Liquibase — Auto-Config Mode (Same Model as Flyway)
Liquibase, when used through Spring Boot’s auto-configuration, behaves identically to Flyway in terms of where enforcement happens: at application boot, before the app is considered “up.” The difference is format — changesets are written in XML, YAML, JSON, or SQL, and tracked in DATABASECHANGELOG / DATABASECHANGELOGLOCK tables instead of Flyway’s single history table.
The meaningful advantage here is built-in rollback. Liquibase changesets can define an explicit rollback block, and liquibase rollback is a free, first-class command — no forward-fix script required. If your team wants DBAs to be able to reverse a change without a developer writing new code, this is the deciding factor over Flyway.
Option 3: Liquibase — Standalone CLI Mode (Pipeline-Enforced)
This is where Liquibase diverges from Flyway entirely. Instead of running inside the application’s boot sequence, Liquibase can run as its own step — invoked from Jenkins, a DBA-controlled job, or any CI system — completely decoupled from the app’s lifecycle.
This is the right model when:
- The service isn’t Java/Spring, so there’s no natural “startup hook” to attach to.
- A central platform/DBA team needs to control when schema changes land, independent of when a given microservice happens to deploy.
- Migrations need to run once against a shared database, not once per ECS task (avoiding N tasks racing to apply the same migration on scale-up).
Here, enforcement isn’t “the app won’t boot without it” — it’s “the pipeline won’t proceed to deploy without it.” The Jenkins stage that runs Liquibase becomes a hard gate: if the changeset fails or the changelog lock can’t be acquired, the deployment stage never fires.
Option 4: Pipeline-Driven Raw SQL / Separate DB-Change Repository
For teams that have already decoupled schema changes from application code — SQL files living in their own repo (e.g. a db-changes repo, organized by ticket ID), rather than inside each service’s codebase — neither Flyway’s nor Liquibase’s “automatic at startup” benefit applies directly, because there’s no single application boot process to hook into.
In this model, enforcement has to be built explicitly into the pipeline:
- Discovery stage — the pipeline diffs the merge to figure out which logical database folders changed (git diff –name-only $(git merge-base …)), rather than relying on a hand-maintained manifest.
- Environment resolution — a single source-of-truth mapping file (e.g. env-db-mapping.json) resolves branch → environment → target database/connection, so there’s no hardcoding of database names per Jenkins stage.
- Execution gate — the pipeline runs the SQL (or Liquibase/Flyway invoked in CLI mode) as a required stage before the deploy stage, using a changelog table of its own to guarantee idempotency and correct ordering, and to prevent the same script running twice.
- Failure = no deploy — if the DB stage fails, the pipeline stops. The application build/deploy stage is never reached.
This is more work to build than “add a library and let it run at boot,” but it’s the only model that fits when scripts are decoupled from application code, when a central team needs release control, or when multiple services/consumers share one database and you don’t want N different app-boot processes independently racing to apply the same migration.
Comparison at a Glance
- Flyway (Spring Boot) — Enforcement happens at app startup, on every task launch. Requires app-repo coupling. No free rollback (forward-fix only; rollback is a paid Teams feature).
- Liquibase (Spring auto-config) — Enforcement happens at app startup, same model as Flyway. Requires app-repo coupling. Rollback is free and built in.
- Liquibase (standalone CLI) — Enforcement happens as an explicit Jenkins/pipeline stage. No app-repo coupling required. Rollback is free and built in.
- Raw SQL / separate DB-change repo — Enforcement happens as an explicit pipeline stage before deploy. No app-repo coupling required. Rollback only exists if you build it in yourself.
Recommendation
- Standard Java/Spring Boot services that own their schema: use Flyway. It’s free forever for the forward-fix pattern, requires no pipeline changes, and is the ecosystem default.
- Same setup, but DBAs need self-service rollback without a developer involved: use Liquibase in Spring auto-config mode instead — same enforcement model, better rollback story.
- Non-Java services, or a central team that must gate schema changes independently of each service’s deploy cadence: use Liquibase in standalone CLI mode, driven as a mandatory Jenkins stage.
- Schema changes already live in a separate repo, decoupled from application code: neither tool’s “automatic at boot” advantage applies as-is. The enforcement has to be engineered into the pipeline itself — discovery, environment resolution, and a hard gate before the deploy stage — which is a structural investment, not a tooling swap.
The common thread across all four options: enforcement only works if there is no path to deployment that skips the schema step. Whether that gate lives inside the JVM’s boot sequence or inside a Jenkins stage is an implementation detail — what matters is that skipping it isn’t an option a person (or a pipeline) can accidentally take.