Skip to main content

Flyway

Flyway versions the database schema alongside the application, so a deployment brings the schema with it instead of relying on someone remembering to run a script.

It records every applied migration in a flyway_schema_history table and refuses to start if what it finds there disagrees with the migrations on the classpath.

Migration types

Versioned

Prefix V, applied exactly once, in version order.

V1.0__create_user_table.sql
V1.1__add_email_column.sql

Versioned migrations

Undo

Prefix U, reverses the versioned migration with the matching version. This is a Flyway Teams (commercial) feature — the open-source edition will not apply them.

U1.1__drop_email_column.sql

Undo migrations

Rolling forward with a new versioned migration is the usual approach on the free edition, and is safer in production regardless: an undo cannot restore data that a DROP COLUMN destroyed.

Repeatable

Prefix R, no version, re-applied whenever the file's checksum changes. Runs after all pending versioned migrations.

R__refresh_views.sql
R__stored_procedures.sql

Repeatable migrations

Suited to objects that can be recreated from scratch — views, stored procedures, functions. Write them with CREATE OR REPLACE, since they will run again.

Naming

V 1.1 __ add_email_column .sql
│ │ │ └── suffix
│ │ └── description; underscores become spaces in the log
│ └── version; dots or underscores separate parts
└── prefix: V versioned, U undo, R repeatable

The separator is two underscores. One underscore is the single most common reason a migration is silently ignored.

Configuration

spring.flyway.enabled=true
spring.flyway.baseline-version=1.0
spring.flyway.target=1.2
# Per-vendor SQL; {vendor} resolves to h2, postgresql, mysql, ...
spring.flyway.locations=classpath:/db/migration/{vendor}

With that locations value, H2 scripts live in src/main/resources/db/migration/h2 and PostgreSQL scripts in .../postgresql. The default without {vendor} is classpath:db/migration.

Splitting by vendor is only necessary where the dialects genuinely differ. Duplicated scripts drift apart, so keep shared migrations in one location and add a vendor-specific location only for the statements that need it:

spring.flyway.locations=classpath:/db/migration/common,classpath:/db/migration/{vendor}

baseline-on-migrate=true is needed when introducing Flyway to a database that already has tables — it records the current state as the baseline instead of failing. Leave it off afterwards; left enabled, it masks a genuinely empty database.

Maintenance

# Inspect the migration state
mvn flyway:info -Ph2 # maps to application-h2.properties
mvn flyway:info -Ppostgre # maps to application-postgre.properties

# Repair the schema history table
mvn flyway:repair

flyway:repair does two things: it removes failed migration entries, and it realigns stored checksums with the files currently on disk. It does not touch the schema itself. Reaching for it because a checksum mismatch appeared is usually wrong — the mismatch means an already-applied migration file was edited, and the correct fix is to restore the file and add a new migration.

Never edit an applied migration. Once it has run anywhere other than your own machine, it is immutable.

Startup fails on checksum mismatch

Validate failed: Migration checksum mismatch for migration version 1.1

Someone changed a file after it was applied. In order of preference:

  • Restore the original file and express the change as a new migration.
  • If the database is disposable (local development), drop it and re-migrate.
  • Only as a last resort, and never automatically in production, run flyway:repair to accept the new checksum.

Setting spring.flyway.validate-on-migrate=false makes the error disappear and the drift permanent. It is not a fix.