Here is the English translation of the article body.
When you manage database schema changes by hand, problems tend to follow: table structures drift between development and production, or SQL scripts get run in the wrong order. This gets even harder when several people are working on the same project and there is no way to trace who changed what, and when.
Flyway lets you version-control your database schema changes the same way you version-control your code. In this article, we’ll walk through how to add Flyway to a Spring Boot project and manage migrations safely.
What Is Flyway?
How Flyway Works
When your application starts (or when you run the CLI), Flyway scans the SQL files under db/migration and sorts them by the version number in each file name. It then consults the flyway_schema_history table to determine which scripts have not yet been applied, runs those SQL scripts inside a transaction, and, on success, inserts a row into the history table recording the version, description, checksum (MD5), execution time, and outcome.
Scripts can use the following prefixes.
| Prefix | Purpose | When It Runs |
|---|---|---|
V (Versioned) | Regular migrations | Once, in ascending version order |
R (Repeatable) | Redefining views, functions, stored procedures | Every time the checksum changes |
U (Undo) | Rollback (Pro/Teams editions only) | When flyway undo is run |
Comparison with Liquibase
Flyway’s strength is its simplicity: you write plain SQL, which means you can take full advantage of database-specific SQL for PostgreSQL, MySQL, and so on. Liquibase abstracts changes into changeSets written in XML or YAML, which enables database-independent schema definitions but comes with a steeper learning curve. For Spring Boot projects that target a single database, Flyway is usually the preferred choice.
Flyway is a tool that automates version management for your database schema. It executes SQL files in order and records which migrations have been applied in the flyway_schema_history table.
Many developers rely on Hibernate’s ddl-auto to generate tables automatically. That is convenient early in development, but in production it carries the risk of unexpected changes. With Flyway, you control schema changes explicitly and share the change history with your whole team, which is much safer.
Adding Flyway to Spring Boot
Let’s start by adding the dependencies. With Gradle, it looks like this.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.flywaydb:flyway-core'
runtimeOnly 'org.postgresql:postgresql'
}
For Maven, use the following.
<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 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 there is no need to write spring.flyway.enabled=true explicitly. Only set it to false if you want to disable Flyway.
Setting ddl-auto=validate turns off Hibernate’s automatic table generation and instead only checks that your entities match the schema. Flyway and Hibernate operate independently, so the recommended pattern is to let Flyway manage the schema and disable Hibernate’s auto DDL (or set it to validate).
For more on data source configuration, see Managing Configuration with application.properties in Spring Boot.
Placing Migration Scripts
Flyway automatically looks in the src/main/resources/db/migration directory, so that’s where your SQL files go.
File names 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 and ascending. Two underscores (__) separate the version from the description. If you don’t follow this naming convention, Flyway won’t recognize the files.
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 runs this script automatically. If you check the database, you should find both a users table and a flyway_schema_history table.
SELECT * FROM flyway_schema_history;
This table records the version, description, execution timestamp, checksum, and other details for each applied migration. If the checksum of a previously recorded script changes, Flyway raises an error and refuses to run, which protects you against tampered history and unintended changes.
Adding Schema Changes
To add a column to an existing table, create a new migration script.
-- V2__add_email_column.sql
ALTER TABLE users ADD COLUMN email VARCHAR(100);
Be careful when adding a NOT NULL constraint in an environment that already has data. The safe approach is to add the column as nullable first, populate a default value, and then switch 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 real projects you’ll need to design appropriate values and a data cleansing strategy based on your business requirements. The example.com domain is reserved by RFC 2606, so it is safe for testing, but adjust it to match your requirements in production.
As for how to group multiple table changes: bundling related changes (such as creating a table and adding its foreign keys) into a single script makes rollback easier. Keeping independent changes in separate scripts makes it easier to isolate problems when something goes wrong.
Applying Flyway to an Existing Database and Per-Environment Settings
If you’re introducing Flyway to a database that already has tables, use baseline-on-migrate.
spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=1
With this setting, Flyway skips all migrations up to the version specified by baseline-version (default is 1) in an existing database environment and adds a baseline record to the history table. After that, only migrations from V2 onward are executed.
In a brand-new environment, on the other hand, all migrations (starting from V1) run as usual. So if you capture your existing table structure as V1__init.sql, you can reproduce the same structure in new environments. In other words, V1 is for building new environments, while baseline is for retrofitting Flyway onto existing ones.
If you specify baseline-version=0, the existing schema is treated as the initial state and migrations are applied starting from V1. With baseline-version=1, V1 is treated as already applied and migrations start from V2. Adjust this to fit your environment.
Production environments call for especially careful configuration. Split your settings into separate files per Profile.
# application-prod.properties
spring.flyway.clean-disabled=true
spring.flyway.baseline-on-migrate=false
Flyway’s clean command is a dangerous feature that drops every table in the database. In production, set clean-disabled=true so it can never be run by accident. In development, leaving it at false gives you the flexibility to reset the schema during testing.
For per-environment configuration, prepare files such as application-dev.properties and application-prod.properties and switch between them with Profiles. For details, see How to Safely Switch Environment-Specific Configuration with Spring Boot Profiles.
Handling Migration Failures
Running flywayRepair
If you run flywayRepair while a failed record remains in the history table, 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
One thing to keep in mind, as the message itself notes, is that flywayRepair only repairs the history table. It does not automatically roll back the partial effects of failed DDL/DML (for example, an ALTER TABLE that ran halfway). On databases like PostgreSQL where DDL is transactional, the change is rolled back automatically on error. On databases like MySQL where DDL is auto-committed, you need to inspect and fix the table state manually.
The following SQL is handy 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 a migration, a record with success=false is left in flyway_schema_history.
SELECT version, description, success FROM flyway_schema_history WHERE success = false;
Checksum mismatch errors occur when you modify a script that has already been applied. In production, the rule is never to modify an existing script. Instead, add your change as a new version.
If you need to fix a script that was committed by mistake during development, follow these steps.
- Fix the script
- Run
./gradlew flywayRepair(ormvn flyway:repair) to recalculate checksums and remove failed records - Restart the application to re-run the migration
The same applies when a migration fails due to a SQL syntax error or a constraint violation. Fix the script, run flywayRepair, and restart.
In development, you could also delete failed records from the history table by hand, but manipulating the history table directly in production is risky. Because of audit trail and integrity concerns, stick to the flywayRepair command.
Team Development and Rollback Strategy
When several people are developing at once, it’s easy to end up creating migrations with the same version number. Sequential numbering (V1, V2…) is simple but prone to collisions across multiple branches. Timestamp-based versions (V20260204120000__…) avoid collisions automatically, at the cost of readability. Choose whichever fits your team’s development flow.
V20260204120000__add_user_email.sql
V20260204130000__create_orders_table.sql
If version numbers collide when merging in Git, rename the file that was created later and bump its version number. Review migration scripts just like code, checking their impact on existing data and the rollback plan.
The free edition of Flyway has no automatic rollback feature. In the Pro/Teams editions, you can write Undo scripts in the U1__, U2__ format and run them with the flyway undo command, but with the free edition you have to prepare separate rollback SQL scripts and run them manually.
The most reliable approach is to take a database backup before applying migrations to production, then restore from that backup if anything goes wrong. Testing thoroughly in a staging environment before applying to production greatly reduces the risk.
Before deploying to production, confirm 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 migrations, check flyway_schema_history to confirm the latest migration shows success=true and verify that the application starts up normally.
For deploying with Docker, see A Practical Guide to Containerizing Spring Boot Applications with Docker.
Summary
With Flyway, the history of your database schema changes becomes clear and you can keep environments consistent. It prevents the human errors that come with running SQL by hand and improves the efficiency of team development.
By following version management rules and testing thoroughly before applying to production, you can greatly reduce deployment risk. Start by trying it out in your development environment and find the workflow that works best for your team.
For entity design in JPA, also see How to Map Entity Relationships with JPA in Spring Boot.