Contents
- Dependency & Auto-Configuration
- Script Naming Convention
- Versioned Migrations
- Repeatable Migrations
- Java-Based Migrations
- Configuration Reference
- Callbacks
- Baseline & Repair
- Testing with Testcontainers
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.
| Prefix | Pattern | Example | Behaviour |
| V | V{version}__{description}.sql | V1__create_users.sql | Runs once, tracked in flyway_schema_history |
| U | U{version}__{description}.sql | U1__undo_create_users.sql | Undo script (Flyway Teams) |
| R | R__{description}.sql | R__seed_lookup.sql | Re-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");
}
}