Translation

Manually managing database schema changes can lead to issues like table structures diverging between development and production environments, or SQL execution order mistakes. This becomes especially problematic when multiple people are working on the same project — without a way to track who made what changes and when, things can get messy.

With Flyway, you can version-control your database schema change history just like you do with code. This article explains how to introduce Flyway into a Spring Boot project and manage migrations safely.

What Is Flyway

How Flyway Works

At application startup (or when run via the CLI), Flyway scans the SQL files under db/migration and sorts them by the version number in the filename. It checks the flyway_schema_history table to identify unapplied scripts, executes the SQL within a transaction, and — if successful — inserts the version, description, checksum (MD5), execution time, and success status into the history table.

Scripts use the following prefixes:

PrefixPurposeExecution Timing
V (Versioned)Regular migrationExecuted once, in ascending version order
R (Repeatable)Redefining views, functions, stored proceduresExecuted every time the checksum changes
U (Undo)Rollback (Pro/Teams edition only)Executed via flyway undo

Comparison with Liquibase

Inserting Pre/Post Processing with Callbacks

Flyway also has a callback feature that lets you hook into processing before and after migrations. Simply place SQL files named beforeMigrate.sql or afterMigrate.sql in src/main/resources/db/migration, and Flyway will automatically detect the timing and run them.

For more complex processing, you can also create a Java class that implements org.flywaydb.core.api.callback.Callback. This approach is well-suited for use cases that are difficult to handle with SQL alone, such as notifying external services or sending data to a logging platform.

For the specific steps to set up Liquibase, how to write changelogs, and how to perform rollbacks, see Managing Database Migrations in Spring Boot with Liquibase.

Flyway’s key feature is the simplicity of writing SQL directly, letting you take full advantage of SQL optimized for a specific database such as PostgreSQL or MySQL. Liquibase abstracts changeSets in XML/YAML, enabling database-agnostic schema descriptions, but this comes with a steeper learning curve. For Spring Boot projects using a single database, Flyway is often the preferred choice.

Flyway is a tool that automates version control of your database schema. It executes SQL files in order and records which migrations have been applied in the flyway_schema_history table.

Many developers use Hibernate’s ddl-auto to auto-generate tables, which is convenient during early development but carries the risk of unexpected changes occurring in production. With Flyway, you can explicitly control schema changes and share the change history across the entire team, making it a safer approach.

Introducing Flyway into Spring Boot

First, let’s add the dependency. For Gradle, add the following.

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 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 there’s no need to explicitly set spring.flyway.enabled=true. Only specify false if you want to disable it.

Setting ddl-auto=validate disables Hibernate’s automatic table generation and simply verifies that the entities match the schema. Since Flyway and Hibernate operate independently, the recommended pattern is to let Flyway manage the schema while disabling Hibernate’s auto-ddl (or setting it to validate).

For more details on data source configuration, see The Basics of Managing Configuration with application.properties in Spring Boot.

Placing Migration Scripts

Flyway automatically looks for the src/main/resources/db/migration directory, so place your SQL files there.

Filenames must follow the format V{version}__{description}.sql.

  • V1__init.sql
  • V2__add_email_column.sql
  • V3__create_orders_table.sql

Note that version numbers must be unique and in ascending order. The description part is separated by two underscores (__). 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 will automatically execute this script. Check the database, and 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 timestamp, checksum, and other details of each applied migration. If the checksum of a script that has already been recorded changes, Flyway will throw an error and refuse to run, preventing tampering with the history or 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);

If you want to add a NOT NULL constraint, be careful in environments that already contain data. The safe approach is to first add the column as nullable, set a default value, 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 to design an appropriate value strategy and data cleansing approach based on your business requirements. example.com is a domain reserved under RFC 2606, so it’s safe for testing purposes — but adjust it to fit your actual requirements in production.

As for how to group multiple table changes together: related changes (such as adding a foreign key alongside creating a table) are best combined into a single script, which makes rollback easier. Independent changes should be kept separate, making it easier to isolate issues if something goes wrong.

Applying to an Existing Database and Environment-Specific Configuration

If you’re introducing Flyway into a database that already has tables, use baseline-on-migrate.

spring.flyway.baseline-on-migrate=true
spring.flyway.baseline-version=1

With this setting, in an existing database environment, migrations up to the version specified by baseline-version (default is 1) are skipped, and a baseline record is added to the history table. After that, only migrations from V2 onward are executed.

On the other hand, in a brand-new environment, all migrations (starting from V1) run normally as usual, so if you recreate the existing table structure as V1__init.sql, you can build the same structure in a new environment. In other words, V1 is for setting up new environments, while baseline is for retroactively introducing Flyway into 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 this according to your environment.

Production environments require particularly careful configuration. Split your 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 drops all tables in the database. In production, set clean-disabled=true to prevent it from being accidentally executed. In development, keeping it false allows you to flexibly reset the schema during testing.

For environment-specific configuration, prepare files like application-dev.properties and application-prod.properties, and switch between them using profiles. For details, see How to Safely Switch Environment-Specific Configuration Using Spring Boot Profiles.

Handling Migration Failures

Example Output from flywayRepair

If you run flywayRepair while a failed record remains, you’ll get output like the following.

$ ./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, as the message itself indicates, that flywayRepair only repairs the history table — it does not automatically roll back the partial effects of a failed DDL/DML statement (for example, an ALTER TABLE that ran halfway through). On databases where DDL is transactional, such as PostgreSQL, changes are automatically rolled back on error. However, on databases where DDL is auto-committed, such as MySQL, you’ll 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;

If an error occurs during migration execution, a record with success=false will remain in flyway_schema_history.

SELECT version, description, success FROM flyway_schema_history WHERE success = false;

A checksum mismatch error occurs when you modify a script that has already been applied. In production, the principle is not to modify existing scripts, but instead to add changes via a new version.

If you need to fix a script that was mistakenly committed during development, follow these steps:

  1. Fix the script
  2. Run ./gradlew flywayRepair (or mvn flyway:repair) to recalculate checksums and remove failed records
  3. Restart the application to re-run the migration

The same applies if a migration fails due to a SQL syntax error or constraint violation — fix the script, run flywayRepair, and restart.

In development, you can also manually delete failed records from the history table, but directly manipulating the history table in production is risky due to audit trail and consistency concerns. Use the flywayRepair command instead.

Team Development and Rollback Strategy

When working with multiple developers, it’s common to accidentally create migrations with the same version number. Sequential numbering (V1, V2…) is simple, but prone to conflicts across multiple branches. Timestamp-based numbering (V20260204120000__…) automatically avoids conflicts, but is less readable. Choose based on your team’s development workflow.

V20260204120000__add_user_email.sql
V20260204130000__create_orders_table.sql

If version numbers conflict during a Git merge, rename the file created later and bump up its version number. Migration scripts should be reviewed just like code — check the impact on existing data and confirm a rollback plan.

Flyway’s free (Community) edition doesn’t have automatic rollback functionality. In the Pro/Teams editions, you can create Undo scripts in the U1__, U2__ format and run them with the flyway undo command, but in the free edition, you need to prepare a separate rollback SQL script and run it manually.

The most reliable approach is to take a database backup before applying changes to production, and restore from the backup if a problem occurs. Thoroughly testing in a staging environment before applying to production can significantly reduce risk.

Before deploying to production, check the following:

  • Has a database backup been taken?
  • Has it been verified to work correctly in staging?
  • Is there a rollback plan in place?
  • Has a maintenance window been secured?

After applying a migration, check flyway_schema_history to confirm that the latest migration has success=true, and verify that the application starts up normally.

For deploying with Docker, see also A Practical Guide to Containerizing Spring Boot Applications with Docker.

Summary

Using Flyway makes your database schema change history explicit and helps maintain consistency across environments. It prevents human error from manually running SQL and improves the efficiency of team development.

By following versioning rules and thoroughly testing before applying changes to production, you can significantly reduce deployment risk. Start by trying it out in a development environment, and find the operational approach that works best for your team.

For JPA entity design, also see How to Map Entity Relationships with JPA in Spring Boot.

When deploying migrations in a Kubernetes environment, you also need to pay attention to the timing of Pod replacement. For setup steps, see How to Deploy a Spring Boot App to Kubernetes, and for configuration that avoids dropping in-flight requests, see Achieving Graceful Shutdown and Zero-Downtime Deployment in Spring Boot.