When manually managing database schema changes, you can run into trouble where table structures differ between development and production environments, or SQL execution order gets mixed up. This is especially problematic when multiple people are developing together — if you can’t track who made what changes and when, things get difficult.
With Flyway, you can version-control database schema changes just like code. This article explains how to introduce Flyway into a Spring Boot project and safely manage migrations.
What is Flyway
How Flyway Works
At application startup (or when run via CLI), Flyway scans SQL files under db/migration and sorts them by version number in the filename. It references the flyway_schema_history table to identify unapplied scripts, executes SQL within a transaction, and on success INSERTs the version, description, checksum (MD5), execution time, and success/failure status into the history table.
Script prefixes come in the following types:
| Prefix | Purpose | Execution Timing |
|---|---|---|
V (Versioned) | Standard migrations | Executed once in ascending version order |
R (Repeatable) | View/function/stored procedure redefinitions | Executed every time the checksum changes |
U (Undo) | Rollback (Pro/Teams editions only) | Executed when flyway undo is run |
Comparison with Liquibase
Flyway’s strength is the simplicity of writing SQL directly, allowing you to leverage SQL optimized for specific databases like PostgreSQL or MySQL. Liquibase abstracts changeSets in XML/YAML, enabling DB-independent schema descriptions, but it has a higher learning curve. For Spring Boot projects using a single DB, Flyway is often the preferred choice.
Flyway is a tool that automates version control of database schemas. It executes SQL files in order and records which migrations have been applied in the flyway_schema_history table.
Many people use Hibernate’s ddl-auto to auto-generate tables, which is convenient in early development but carries the risk of unexpected changes in production. With Flyway, you can explicitly control schema changes and share the change history across the team, making it safer.
Introducing Flyway to Spring Boot
First, add the dependencies. For Gradle, write it like this:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.flywaydb:flyway-core'
runtimeOnly 'org.postgresql:postgresql'
}
For Maven, use this:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
Next, configure the database connection and Flyway settings in application.properties.
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=user
spring.datasource.password=pass
spring.jpa.hibernate.ddl-auto=validate
Flyway is enabled by default in Spring Boot 3.x, so you don’t need to explicitly write spring.flyway.enabled=true. Specify false only when you want to disable it.
Setting ddl-auto=validate disables Hibernate’s automatic table generation and just verifies that entities match the schema. Since Flyway and Hibernate operate independently, the recommended pattern is to manage the schema with Flyway and disable Hibernate’s auto-ddl (or set it to validate).
For more details on data source configuration, see Spring Bootのapplication.propertiesで設定を管理する基本.
Placing Migration Scripts
Flyway automatically looks in the src/main/resources/db/migration directory, so place your SQL files there.
Filenames must follow the format V{version}__{description}.sql.
V1__init.sqlV2__add_email_column.sqlV3__create_orders_table.sql
Note that version numbers must be unique in ascending order. The description is separated by two underscores (__). Flyway won’t recognize files that don’t follow this naming convention.
Creating the Initial Schema
Let’s create the first migration script, V1__init.sql.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_username ON users(username);
When you start the Spring Boot application, Flyway automatically executes this script. If you check, you should find both the users table and the flyway_schema_history table.
SELECT * FROM flyway_schema_history;
This table records the version, description, execution date/time, checksum, and other details of applied migrations. Once a script’s checksum is recorded, if it changes Flyway throws an error and refuses to run, preventing history tampering or unintended changes.
Adding Schema Changes
When adding a column to an existing table, create a new migration script.
-- V2__add_email_column.sql
ALTER TABLE users ADD COLUMN email VARCHAR(100);
When adding a NOT NULL constraint, be careful in environments with existing data. The safe approach is to first add the column allowing NULLs, set default values, and then change it to NOT NULL.
-- V3__make_email_required.sql
UPDATE users SET email = '[email protected]' WHERE email IS NULL;
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
This example uses a placeholder default value, but in practice you’ll need appropriate value design and data cleansing strategies based on business requirements. example.com is reserved by RFC2606 so it’s safe for testing, but adjust as needed for production.
How to group multiple table changes is up to you, but combining related changes (such as adding a foreign key with creating a table) into one script makes rollback easier. Splitting independent changes makes it easier to isolate issues when problems occur.
Applying to Existing Databases and Environment-Specific Configuration
When introducing Flyway to a database where tables already exist, use baseline-on-migrate.
spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=1
With this setting, existing database environments skip migrations up to the version specified by baseline-version (default is 1) and add a baseline record to the history table. After that, only migrations from V2 onward are executed.
On the other hand, in a fresh new environment, all migrations (from V1) are executed normally, so if you reproduce the existing table structure as V1__init.sql, you can build the same structure in new environments. In short, V1 is for building new environments, while baseline is for retrofitting Flyway to existing environments.
Specifying baseline-version=0 treats the existing schema as the initial state and starts applying from V1. With baseline-version=1, V1 is treated as existing and application starts from V2. Adjust according to your environment.
Production environments require especially careful configuration. Split configuration files by Profile.
# application-prod.properties
spring.flyway.clean-disabled=true
spring.flyway.baseline-on-migrate=false
Flyway’s clean command is a dangerous feature that deletes all tables in the database. In production, set clean-disabled=true to prevent accidental execution. In development environments, setting it to false allows you to flexibly reset the schema during testing.
For environment-specific configuration, prepare application-dev.properties and application-prod.properties and switch between them with Profiles. For details, see Spring BootのProfileを使って環境によって違う設定を安全に切り替える方法.
Handling Migration Failures
Example of Running flywayRepair
When running flywayRepair with failed records remaining, you’ll get output like this:
$ ./gradlew flywayRepair
> Task :flywayRepair
Database: jdbc:postgresql://localhost:5432/mydb (PostgreSQL 15.4)
Successfully repaired schema history table "public"."flyway_schema_history" (execution time 00:00.045s).
Manual cleanup of the remaining effects of the failed migration may still be required.
BUILD SUCCESSFUL
Note that as the message indicates, flywayRepair only repairs the history table — partial effects from failed DDL/DML (such as a partially-applied ALTER TABLE) are not automatically rolled back. In databases where DDL is transactional like PostgreSQL, automatic rollback occurs on error, but in databases where DDL is auto-committed like MySQL, you need to manually check and fix the table state.
The following SQL is useful for checking the current state of the history table:
SELECT installed_rank, version, description, type, checksum, success, installed_on
FROM flyway_schema_history
ORDER BY installed_rank DESC
LIMIT 10;
When an error occurs during migration execution, a record with success=false remains in flyway_schema_history.
SELECT version, description, success FROM flyway_schema_history WHERE success = false;
Checksum mismatch errors occur when you modify an already-applied script. The rule in production is not to modify scripts, but to add changes in a new version.
If you want to fix a script accidentally committed during development, follow these steps:
- Modify the script
- Run
./gradlew flywayRepair(ormvn flyway:repair) to recalculate checksums and remove failed records - Restart the application to re-run migrations
The same applies if a migration fails due to SQL syntax errors or constraint violations. Fix the script, run flywayRepair, and restart — that’s it.
In development environments, you can also manually delete failed records from the history table, but in production it’s dangerous to directly manipulate the history table. There are audit trail and integrity risks, so use the flywayRepair command instead.
Team Development and Rollback Strategy
When multiple people are developing, you may end up creating migrations with the same version number. Sequential numbering (V1, V2…) is simple but prone to duplication across multiple branches. Timestamp-based naming (V20260204120000__…) automatically avoids duplication, but reduces readability. Choose based on your team’s development flow.
V20260204120000__add_user_email.sql
V20260204130000__create_orders_table.sql
When merging in Git, if version numbers are duplicated, rename the file created later to bump its version number. Migration scripts should be reviewed like code, checking impact on existing data and rollback plans.
The free version of Flyway doesn’t have automatic rollback functionality. Pro/Teams editions allow you to create Undo scripts in U1__, U2__ format and run them with the flyway undo command, but in the free version you need to prepare rollback SQL scripts separately and execute them manually.
The most reliable approach is to take a database backup before production deployment and restore from backup if problems occur. Thoroughly validating in a staging environment before applying to production significantly reduces risk.
Before deploying to production, check the following:
- Has a database backup been taken?
- Did it work correctly in the staging environment?
- Is a rollback plan in place?
- Has a maintenance window been secured?
After applying the migration, check flyway_schema_history to confirm that the latest migration has success=true and that the application starts normally.
For deployment using Docker, see Spring BootアプリケーションをDockerでコンテナ化する実践ガイド.
Summary
Frequently Asked Questions (FAQ)
What is Flyway?
Flyway is a migration tool that manages database schema changes as version-controlled SQL files and automatically applies them at application startup. Since it records the version, checksum, and execution date/time of applied scripts in the flyway_schema_history table, schema consistency is maintained across development, staging, and production environments. Unlike Hibernate’s ddl-auto, you can explicitly manage changes in SQL, making it suitable for production operation.
How is Flyway different from Liquibase?
Flyway is SQL-file-centric and simple, while Liquibase offers DB-independent abstraction in XML/YAML/JSON. For Spring Boot projects that continue to use specific DBs like PostgreSQL or MySQL, Flyway is often chosen; if there’s a possibility of switching between multiple DB engines, Liquibase is often selected.
What’s the difference between baseline-version=0 and baseline-version=1?
baseline-version=0 treats the existing schema as an “initial state with nothing applied” and runs all migrations from V1. baseline-version=1 treats V1 as equivalent to the existing schema and only runs V2 and later. When retrofitting Flyway to an environment that already has tables in production, baseline-version=1 + baseline-on-migrate=true is the common combination.
When should I run flywayRepair?
Use it in the following cases: (1) when a SQL error occurs during migration execution and a success=false record remains in flyway_schema_history, or (2) when you modified an already-applied migration file during development and a checksum mismatch occurs. Running ./gradlew flywayRepair (or mvn flyway:repair) removes failed records and recalculates checksums. As a rule, don’t run it in production — add fixes in a new version instead.
Can I manually edit the flyway_schema_history table?
Not recommended. This table is the basis for Flyway’s integrity checks, and directly running UPDATE/DELETE breaks the audit trail and leads to undefined migration behavior thereafter. If you want to reset in a development environment, use flyway clean; if you need recovery in production, use flywayRepair.
Is spring.flyway.enabled=true required in Spring Boot?
Not required. In Spring Boot 3.x, adding org.flywaydb:flyway-core or org.springframework.boot:spring-boot-starter-flyway as a dependency automatically enables it. Specify spring.flyway.enabled=false explicitly only when you want to disable it.
By using Flyway, the history of database schema changes becomes clear, and you can maintain consistency across environments. It prevents human errors from manually executing SQL and improves team development efficiency.
By following version control rules and thoroughly validating before production deployment, you can significantly reduce deployment risk. Try it first in a development environment and find an operational approach that suits your team.
For JPA entity design, also see Spring BootのJPAでエンティティのリレーションシップをマッピングする方法.