Contents

Add the following to your pom.xml (Maven) or build.gradle (Gradle). Spring Boot manages compatible versions automatically when you use the Spring Boot BOM.

<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency> <!-- For MySQL / MariaDB --> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-mysql</artifactId> </dependency> <!-- For SQL Server --> <dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-sqlserver</artifactId> </dependency>

Spring Boot auto-detects Flyway on the classpath and runs all pending migrations before the application context finishes starting. No @EnableFlyway annotation is needed.

# application.yml — minimal config (datasource is all that's required) spring: datasource: url: jdbc:postgresql://localhost:5432/mydb username: app password: secret flyway: enabled: true # default — set false to disable locations: classpath:db/migration # default script location baseline-on-migrate: false # set true for existing databases (see Baseline section)

Flyway identifies scripts by their filename. The naming pattern determines how each script is treated.

PrefixPatternExampleBehaviour
VV{version}__{description}.sqlV1__create_users.sqlRuns once, tracked in flyway_schema_history
UU{version}__{description}.sqlU1__undo_create_users.sqlUndo script (Flyway Teams)
RR__{description}.sqlR__seed_lookup.sqlRe-runs when checksum changes
src/main/resources/db/migration/ ├── V1__create_users_table.sql ├── V2__add_email_index.sql ├── V3__create_orders_table.sql ├── V4__add_order_status_column.sql └── R__populate_status_lookup.sql

Execute the SQL statements below against your target database. Review each statement before running in production.

-- V1__create_users_table.sql CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- V2__add_email_index.sql CREATE INDEX idx_users_email ON users (email); -- V3__create_orders_table.sql CREATE TABLE orders ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), status VARCHAR(20) NOT NULL DEFAULT 'PENDING', total NUMERIC(12,2), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- V4__add_order_status_column.sql ALTER TABLE orders ADD COLUMN notes TEXT; ALTER TABLE orders ADD CONSTRAINT chk_status CHECK (status IN ('PENDING','CONFIRMED','SHIPPED','DELIVERED','CANCELLED'));

Flyway records each applied migration in flyway_schema_history with the version, description, checksum, and execution time. If a previously applied script's checksum changes, Flyway throws an error to prevent silent schema drift.

Repeatable migrations (prefix R__) are re-applied whenever their checksum changes. Use them for views, stored procedures, and reference data that you want to keep in sync with the code.

-- R__create_order_summary_view.sql -- Flyway re-runs this whenever the file content changes CREATE OR REPLACE VIEW order_summary AS SELECT u.username, COUNT(o.id) AS total_orders, SUM(o.total) AS lifetime_value, MAX(o.created_at) AS last_order_at FROM users u LEFT JOIN orders o ON o.user_id = u.id GROUP BY u.id, u.username; -- R__seed_status_lookup.sql -- Safe upsert so re-running doesn't duplicate rows INSERT INTO status_lookup (code, label) VALUES ('PENDING', 'Pending'), ('CONFIRMED', 'Confirmed'), ('SHIPPED', 'Shipped'), ('DELIVERED', 'Delivered'), ('CANCELLED', 'Cancelled') ON CONFLICT (code) DO UPDATE SET label = EXCLUDED.label;

When SQL is not enough — for example, to encrypt existing data or perform complex transformations — implement JavaMigration. Spring beans can be injected via the constructor.

// V5__encrypt_user_emails.java // Place in the same package as configured in spring.flyway.locations (classpath:db/migration) @Component public class V5__encrypt_user_emails implements JavaMigration { // Spring Boot 3.x: inject beans by making the migration a Spring component private final EncryptionService encryptionService; public V5__encrypt_user_emails(EncryptionService encryptionService) { this.encryptionService = encryptionService; } @Override public MigrationVersion getVersion() { return MigrationVersion.fromVersion("5"); } @Override public String getDescription() { return "Encrypt existing user emails"; } @Override public Integer getChecksum() { return 1; // bump to re-run } @Override public boolean isBaselineMigration() { return false; } @Override public boolean canExecuteInTransaction() { return true; } @Override public void migrate(Context context) throws Exception { try (var stmt = context.getConnection().createStatement(); var rs = stmt.executeQuery("SELECT id, email FROM users")) { while (rs.next()) { long id = rs.getLong("id"); String encrypted = encryptionService.encrypt(rs.getString("email")); try (var upd = context.getConnection().prepareStatement( "UPDATE users SET email = ? WHERE id = ?")) { upd.setString(1, encrypted); upd.setLong(2, id); upd.executeUpdate(); } } } } } # Register Java migrations location spring: flyway: locations: - classpath:db/migration # SQL scripts - classpath:db/javamigration # Java migrations

The settings below can be placed in application.yml or application.properties. All keys shown are optional unless marked — Spring Boot applies sensible defaults when they are omitted.

spring: flyway: enabled: true locations: classpath:db/migration schemas: public # schemas to manage table: flyway_schema_history # history table name baseline-on-migrate: false # true = treat existing DB as already at baseline-version baseline-version: 0 # version used for baseline baseline-description: "Initial baseline" out-of-order: false # allow migrations with lower version than latest applied validate-on-migrate: true # fail if checksum mismatch detected clean-disabled: true # IMPORTANT: prevent accidental DROP in production placeholders: # ${placeholder} substitution in SQL scripts schema: public tablePrefix: app_ target: latest # stop at a specific version (e.g., "3") for testing Always set spring.flyway.clean-disabled=true in production. The flyway:clean command drops everything in the managed schemas — destructive and irreversible.

Callbacks let you hook into Flyway lifecycle events — before migration, after each migration, on validation error, and more.

@Component public class FlywayAuditCallback implements Callback { private static final Logger log = LoggerFactory.getLogger(FlywayAuditCallback.class); @Override public boolean supports(Event event, Context context) { return event == Event.AFTER_EACH_MIGRATE || event == Event.AFTER_MIGRATE_ERROR; } @Override public boolean canHandleInTransaction(Event event, Context context) { return true; } @Override public void handle(Event event, Context context) { MigrationInfo info = context.getMigrationInfo(); if (event == Event.AFTER_EACH_MIGRATE) { log.info("Migration applied: {} — {} in {}ms", info.getVersion(), info.getDescription(), info.getExecutionTime()); } else { log.error("Migration FAILED: {} — {}", info.getVersion(), info.getDescription()); } } @Override public String getCallbackName() { return "FlywayAuditCallback"; } }

Baseline — use when Flyway is introduced into an existing database that already has a schema. Baseline stamps the current state as version N without running any scripts up to that version.

# First deployment on an existing database spring: flyway: baseline-on-migrate: true baseline-version: 1 # marks existing DB as "at V1" baseline-description: "Existing schema before Flyway" # Or via CLI flyway -url=jdbc:postgresql://... -user=... baseline -baselineVersion=1

Repair — fixes failed migrations (removes them from the history table) and recalculates checksums after a script is edited. Run repair before retrying a failed migration.

// Programmatic repair in a Spring Boot @Bean or @PostConstruct @Bean public FlywayMigrationStrategy flywayRepairStrategy() { return flyway -> { flyway.repair(); // fix history table flyway.migrate(); // then apply pending }; }

Testcontainers spins up a real database (or broker) in Docker for each test run, giving you integration-test confidence without a shared external service. The @DynamicPropertySource method wires the container's JDBC URL into Spring's environment before the application context starts.

<dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <scope>test</scope> </dependency> @SpringBootTest @Testcontainers class FlywayMigrationTest { @Container static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16-alpine") .withDatabaseName("testdb") .withUsername("test") .withPassword("test"); @DynamicPropertySource static void configureProperties(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", postgres::getJdbcUrl); registry.add("spring.datasource.username", postgres::getUsername); registry.add("spring.datasource.password", postgres::getPassword); } @Autowired private JdbcTemplate jdbcTemplate; @Test void allMigrationsApplyCleanly() { // If Flyway fails, the context won't start and the test fails automatically. // Verify the expected schema exists: Integer count = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'users'", Integer.class); assertThat(count).isEqualTo(1); } @Test void schemaHistoryContainsAllVersions() { List<String> versions = jdbcTemplate.queryForList( "SELECT version FROM flyway_schema_history WHERE success = true ORDER BY installed_rank", String.class); assertThat(versions).containsExactly("1", "2", "3", "4"); } }