# 3. Why migrations run outside the app [← 2. The expand migration](02-the-expand-migration.md) · [Next: 4. The dual write →](04-the-dual-write.md) Every other module in this repository lets Spring Boot run Flyway on startup — `spring.flyway.enabled=true`, migrate-on-boot, the default most tutorials show. This module turns that off: ```yaml flyway: enabled: false ``` from [`application.yml`](https://ankurm.com/git.app/asmhatre/spring-boot-demo/src/branch/main/db-migrations-expand-contract/src/main/resources/application.yml), and instead ships a second, standalone entry point: [`MigrationCli`](https://ankurm.com/git.app/asmhatre/spring-boot-demo/src/branch/main/db-migrations-expand-contract/src/main/java/com/ankurm/expandcontract/migration/MigrationCli.java): ```java Flyway flyway = Flyway.configure() .dataSource(url, "sa", "") .locations("classpath:db/migration") .target(target) .load(); flyway.migrate(); ``` invoked by [`scripts/migrate.sh`](https://ankurm.com/git.app/asmhatre/spring-boot-demo/src/branch/main/db-migrations-expand-contract/scripts/migrate.sh): ```bash java -cp "$MODULE_DIR/target/classes:$(cat "$CP_FILE")" \ com.ankurm.expandcontract.migration.MigrationCli --target="$TARGET" ``` Why bother, when "migrate on boot" is one line of config? Because "migrate on boot" quietly welds a schema change to an application restart, and expand-contract's whole argument is that those two things need to be independently controllable events: - Deploy 1 (expand) and Deploy 4b (contract) run a migration with **zero** app restarts — every replica keeps serving traffic on its current code the entire time the `ALTER TABLE` executes. `scripts/run-all.sh` calls `migrate.sh` directly for both of these, with no `stop-instance.sh` / `start-instance.sh` anywhere nearby. - Deploys 2, 3, and 4a are **pure code deploys** — a rolling restart with `--target` fixed at whatever the schema already is. No new SQL runs. If Flyway ran on every boot, a canary replica restarting for an unrelated reason (an OOM, a node reschedule, a routine redeploy of a config value) would silently re-run whatever migrations hadn't executed yet, at a moment nobody chose. Running Flyway from its own process, invoked deliberately by the deploy pipeline (or by hand, as this module's scripts do), means a schema change happens exactly once, at exactly the moment someone decided it should — the same discipline a real CI/CD "run migrations" job step gives you, kept intact here even though this whole sequence runs on one sandbox. The `--target` flag is what lets `migrate.sh 2` mean "get the schema to exactly V2, no further" — the same `spring.flyway.target` property [`TestSupport.migrateTo`](https://ankurm.com/git.app/asmhatre/spring-boot-demo/src/branch/main/db-migrations-expand-contract/src/test/java/com/ankurm/expandcontract/TestSupport.java) uses to put a test database at "however far Deploy N has gotten" before exercising `CustomerService` against it. ## Going deeper - `MigrationCli` connects to the same standalone H2 TCP server the app replicas do — [chapter 12](12-the-auto-server-trap.md) covers why that database is its own process rather than something either replica owns. - Flyway's own migrate-on-startup vs. separate-migration-step tradeoff is discussed in [Flyway's documentation on migrations](https://documentation.red-gate.com/fd/migrations-184127470.html) (nofollow). [← 2. The expand migration](02-the-expand-migration.md) · [Next: 4. The dual write →](04-the-dual-write.md)