Flyway with Spring Boot

Flyway with Spring Boot: Seamless Database Migrations

Learn Flyway with Spring Boot with multi-environment support. This detailed guide covers configuration using Spring profiles, environment-specific scripts, and automated database migrations.

1. Introduction

In the world of modern software development, managing database schema changes across multiple environments can be a significant challenge. In our earlier blog on Flyway with the Spring Boot Maven plugin, we explored how to gain granular control over migrations using Maven commands. That approach is powerful for CI/CD pipelines and manual execution.

However, Spring Boot also offers fantastic out-of-the-box support for Flyway, which automates migrations on application startup. This integrated approach simplifies the development workflow by ensuring the database is always in sync with the application code.

This guide will walk you through setting up a sophisticated, multi-environment database migration strategy using Flyway’s native Spring Boot integration. We will build a simple application that leverages Spring Profiles to manage distinct migration paths for development, staging, and production environments, ensuring a consistent and error-free process.

2. The Core Idea: Flyway with Spring Boot

Unlike the Maven-driven approach where you explicitly run commands like mvn flyway:migrate, Spring Boot’s auto-configuration handles this for you. When Spring Boot detects Flyway on the classpath, it automatically wires it into the application lifecycle.By default, it will:

  • Enable Flyway: spring.flyway.enabled is true by default.
  • Locate Migrations: Scan the classpath:db/migration folder for SQL scripts.
  • Run Migrations: Execute any pending migrations against the primary data source when the application starts.

We will harness this automation and combine it with Spring Profiles to create a powerful multi-environment setup. By defining profile-specific configuration files (application-dev.ymlapplication-stage.ymlapplication-prod.yml), we can instruct Flyway to use different database connections and, more importantly, different sets of migration scripts for each environment.

3. Key advantages

Key advantages of using Flyway with Spring Boot:

  • Automatic Discovery: Migration scripts are automatically detected from classpath locations.
  • Environment-Specific Configuration: Easy multi-environment setup through Spring profiles.
  • Zero External Tools: No need for separate Maven commands or external tools.
  • Integration with Spring Actuator: Built-in health checks and migration monitoring.
  • Seamless DataSource Integration: Automatic configuration with Spring Boot’s DataSource.


4. Let’s Get Practical: Flyway with Spring Boot

Now, let’s put our knowledge into practice by building a simple Spring Boot application that uses Flyway for database migrations.

Project Structure:

flyway-spring-boot
├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── bootcamptoprod
│       │           ├── controller
│       │           │   └── UserController.java
│       │           ├── entity
│       │           │   └── User.java
│       │           ├── repository
│       │           │   └── UserRepository.java
│       │           └── FlywaySpringBootApplication.java
│       └── resources
│           ├── application.yml
│           ├── application-dev.yml
│           ├── application-stage.yml
│           ├── application-prod.yml
│           └── db
│               └── migration
│                   ├── common
│                   │   ├── V1__Create_users_table.sql
│                   │   └── V2__Add_email_column.sql
│                   ├── dev
│                   │   └── V3__Insert_dev_test_data.sql
│                   ├── stage
│                   │   └── V3__Insert_stage_test_data.sql
│                   └── prod
│                       └── V3__Insert_prod_test_data.sql
└── pom.xml
Project Structure

🔍 Understanding the Flyway with Spring Boot Project Structure

Here is a quick breakdown of the key files and directories in our project and what each one does. This structure is designed to cleanly separate application code, universal and environment-specific database migrations, and all configuration within the Spring Boot ecosystem.

🚀 Spring Boot Application (src/main/java)

  • FlywaySpringBootDemoApplication.java: The main class that bootstraps and runs the entire Spring Boot application. It’s the entry point that triggers the auto-configuration, including Flyway’s migration process.
  • User.java: A JPA entity that maps directly to the users database table. The columns and constraints defined in this class must be kept in sync with the schema created by our Flyway scripts.
  • UserRepository.java: A Spring Data JPA repository interface. It provides standard database operations (like findAll(), findById(), etc.) for the User entity without requiring us to write any boilerplate code.
  • UserController.java: The REST controller that exposes /api/users endpoints. We will use this to verify that the data inserted by our Flyway migrations is correctly retrieved from the database.

✈️ Flyway Migrations (src/main/resources/db/migration)

This is the heart of our database versioning system. Flyway, by convention, automatically scans this classpath location for SQL scripts. We organize our migrations into subdirectories to manage different environments cleanly.

  • /common: This directory contains migration scripts that are universal and must be applied to all environments (dev, stage, and prod). This is where we define the core schema, like creating tables (V1__…) and altering them (V2__…).
  • /dev, /stage, /prod:  These directories hold environment-specific scripts. Each contains a V3__… script designed to run only in its corresponding environment. This is perfect for inserting different sets of sample data (e.g., extensive mock data for dev, curated QA data for stage, and essential seed data for prod).

⚙️ Environment Configuration (src/main/resources)

Instead of using external configuration files or complex Maven settings, we leverage the power of Spring Profiles. The configuration for each environment is self-contained within profile-specific YAML files.

  • application-dev.yml: Contains the configuration exclusively for the development environment. It defines the dev database connection details and, most importantly, sets the spring.flyway.locations property to scan both the common and dev migration folders.
  • application-stage.yml: Contains the configuration for the staging or UAT environment. It points to the staging database and configures Flyway to use scripts from the common and stage folders.
  • application-prod.yml: Contains the configuration for the production environment. It directs Flyway to use the common and prod scripts. It also includes extra safety measures, like disabling the clean command.

📄 Project Configuration Files

These are the top-level configuration files for our project.

  • application.yml: This is the primary, or base, configuration file. It holds properties that are shared across all profiles, such as the application name and some other config applicable across all environments.
  • pom.xml: This is the master file for our Maven project. Its main role is dependency management. It defines all the necessary libraries our project needs, including Spring Boot starters, the PostgreSQL driver, and flyway-core. 


Let’s set up our project with the necessary dependencies and configurations.

Step 1: Add Maven Dependencies

Every Spring Boot project starts with the pom.xml file. This is where we tell Maven which libraries our project needs to function.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.bootcamptoprod</groupId>
    <artifactId>flyway-spring-boot</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>flyway-spring-boot</name>
    <description>A simple project demonstrating multi-environment database migrations using Flyway and Spring Boot
    </description>

    <properties>
        <java.version>21</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-database-postgresql</artifactId>
        </dependency>

        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
pom.xml

Explanation:

The pom.xml file’s primary role is to define the project’s dependencies. The <dependencies> section lists all the required libraries for our application to function.

  • spring-boot-starter-web: Includes all necessary components for building web applications and RESTful APIs, including an embedded Tomcat server.
  • spring-boot-starter-data-jpa: Provides support for database persistence using Spring Data JPA with Hibernate as the default implementation.
  • spring-boot-starter-actuator: Adds production-ready monitoring features, such as health checks (/actuator/health) and application info endpoints.
  • flyway-core: This is the essential Flyway library. Its presence on the classpath enables Spring Boot’s auto-configuration to automatically run database migrations on application startup.
  • flyway-database-postgresql: A specific Flyway module that provides enhanced support for the PostgreSQL database dialect.
  • postgresql: The JDBC driver that allows the application to connect and communicate with a PostgreSQL database at runtime.

Step 2: Multi-Environment Configuration (The YAML Files)

This is the heart of our setup. We leverage Spring Profiles to create a clean, powerful, and environment-aware configuration. We’ll use a base application.yml file for shared settings and profile-specific files (application-dev.yml, application-stage.yml, application-prod.yml) to override and add environment-specific details.

application.yml (Base Configuration)

This file acts as the foundation, defining all the properties that are common across every environment. It sets up sensible defaults for JPA, Flyway, and monitoring, ensuring consistency.

spring:
  application:
    name: flyway-spring-boot

  profiles:
    active: dev

  jpa:
    hibernate:
      ddl-auto: validate  # Important: Only validate, let Flyway handle schema
    show-sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true

  flyway:
    enabled: true
    baseline-on-migrate: true
    validate-on-migrate: true
    out-of-order: false
    group: true

# Actuator endpoints for monitoring Flyway
management:
  endpoints:
    web:
      exposure:
        include: health,flyway,metrics
  endpoint:
    health:
      show-details: always

# (Optional) For detailed logging
logging:
  level:
    org.flywaydb: INFO
    org.springframework.jdbc: INFO
application.yaml

📄 Explanation:

  • Shared Settings: This file contains global configurations for JPA, Flyway, and Actuator that apply to all environments.
  • jpa.hibernate.ddl-auto: validate: This is a critical safety setting. It instructs Hibernate not to create or modify the database schema. Instead, it only validates that your JPA entities (@Entity classes) match the schema that Flyway has created. If there’s a mismatch, the application will fail to start.
  • flyway.*: These properties enable Flyway and configure its core behavior, such as automatically creating a baseline for an existing database (baseline-on-migrate) and validating scripts before migration (validate-on-migrate).
  • management.endpoints.*: This exposes the /actuator/flyway endpoint, which allows you to monitor the status of your migrations through an API call.

application-dev.yml (Development Profile)

This file is tailored specifically for the local development workflow. It connects to a local database and enables features that are useful for developers, like the ability to reset the database.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/${DB_NAME:flyway_demo_dev}
    username: ${DB_USER:postgres}
    password: ${DB_PASSWORD:postgres}
    driver-class-name: org.postgresql.Driver

  flyway:
    locations: classpath:db/migration/common,classpath:db/migration/dev
    clean-disabled: false  # Allow clean in development
application-dev.yaml

📄 Explanation:

  • Development Environment: This configuration is activated when the dev profile is active.
  • Local Database: It configures the connection to a local PostgreSQL database, using environment variables with sensible defaults for flexibility.
  • flyway.locations: This is the key property for multi-environment migrations. It tells Flyway to scan for and apply SQL scripts from both the common and the dev folders.
  • flyway.clean-disabled: false: This setting explicitly allows the use of the flyway clean, which is very useful during development for quickly wiping and resetting the database.

application-stage.yml (Development Profile)

This file configures the application for a shared pre-production environment, such as Staging or User Acceptance Testing (UAT). The focus here shifts towards stability and safety.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/${DB_NAME:flyway_demo_stage}
    username: ${DB_USER:postgres}
    password: ${DB_PASSWORD:postgres}
    driver-class-name: org.postgresql.Driver

  flyway:
    locations: classpath:db/migration/common,classpath:db/migration/stage
    clean-disabled: true  # Prevent accidental clean in staging
application-stage.yaml

📄 Explanation:

  • Staging/UAT Environment: This configuration is used when the stage profile is active.
  • Staging Database: It points the application to the dedicated staging database server.
  • flyway.locations: It instructs Flyway to apply scripts from the common folder, followed by scripts from the stage folder, ensuring a consistent base schema with curated test data.
  • flyway.clean-disabled: true: This is an important safety measure. It disables the clean command to prevent any developer from accidentally wiping a shared database that the QA team relies on.

application-prod.yml (Production Profile)

This configuration is for the live production environment and prioritizes maximum safety and reliability.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/${DB_NAME:flyway_demo_prod}
    username: ${DB_USER:postgres}
    password: ${DB_PASSWORD:postgres}
    driver-class-name: org.postgresql.Driver

  flyway:
    locations: classpath:db/migration/common,classpath:db/migration/prod
    clean-disabled: true  # Never allow clean in production
    validate-on-migrate: true
application-prod.yaml

📄 Explanation:

  • Production Environment: This is the configuration for your live application, activated with the prod profile.
  • Production Database: It contains the connection details for the production database, which should ideally be loaded from secure environment variables.
  • flyway.locations: It ensures that only scripts from the common and prod folders are applied, providing the stable production schema with any necessary seed data.
  • flyway.clean-disabled: true: This is a critical safeguard. Disabling the clean command in production is non-negotiable to prevent catastrophic data loss.
  • flyway.validate-on-migrate: true: Although this is the default in our base config, explicitly setting it here serves as a clear declaration and a final safety check before any production migration runs.


Step 3: Application Entry Point

This is the main class that bootstraps our entire application.

package com.bootcamptoprod;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FlywaySpringBootApplication {

    public static void main(String[] args) {
        SpringApplication.run(FlywaySpringBootApplication.class, args);
    }

}
FlywaySpringBootApplication.java

Explanation:

  • Application Entry Point: FlywaySpringBootDemoApplication is the main class that serves as the starting point for our application. When executed, its main method bootstraps the entire Spring Boot application. This initialization process triggers Flyway’s auto-configuration, which automatically runs any pending database migrations to ensure the schema is up-to-date before the embedded web server starts and the application begins accepting requests.

Step 4: Create User Entity

This class acts as a blueprint for a user. It defines the structure of a user record, with fields like id, name, and email, and maps directly to the columns in our users database table.

package com.bootcamptoprod.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(unique = true)
    private String email;

    // Constructors
    // Getters and Setters
}
User.java

Step 5: The User Repository

This interface is our data access layer. It provides all the necessary methods to interact with the users table in the database, such as finding, saving, and deleting user records, without us having to write the actual SQL code.

package com.bootcamptoprod.repository;

import com.bootcamptoprod.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}
UserRepository.java


Step 6: The Controller Layer: API Endpoint

This controller class will expose our GET endpoints that return the data that is stored inside our database.

package com.bootcamptoprod.controller;

import com.bootcamptoprod.entity.User;
import com.bootcamptoprod.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<User> getUserById(@PathVariable Long id) {
        return userRepository.findById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }
}
UserController.java

Explanation:

  • This class is the public face of our application’s API, defining the web endpoints for user data.
  • It exposes two distinct GET endpoints: one to retrieve a complete list of all users, and another to fetch a single, specific user by their ID.

Step 7: The Multi-Environment Migration Structure

A key to robust database management is separating universal schema changes from environment-specific data. Our project achieves this by organizing SQL migration scripts into distinct folders, which will be selectively applied based on the active profile. This ensures a consistent base schema everywhere, while allowing for tailored data in each environment.

Step 7.1. /common Scripts (The Foundation)

This folder contains the core, foundational schema changes that must be applied to all environments in a precise order.

  • V1__Create_users_table.sql: This is the first migration. It builds the initial users table with its primary key and essential columns.
  • V2__Add_email_column.sql: This script alters the existing users table to add a new email column with a unique constraint, a change needed across every environment.
-- Create users table
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
V1__Create_users_table.sql
-- Add email column with unique constraint
ALTER TABLE users ADD COLUMN email VARCHAR(255);

-- Add unique constraint
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
V2__Add_email_column.sql

Step 7.2. /dev Scripts (For Local Development)

This folder contains migrations that run only in the development environment, after all the common scripts.

  • V3__Insert_dev_test_data.sql: This script populates the database with a generous amount of mock data (e.g., ‘John Doe Dev’, ‘Jane Smith Dev’). This is perfect for local testing, allowing developers to work with the API without having to manually insert records.
-- Insert development test data
INSERT INTO users (name, email) VALUES
    ('John Doe Dev', 'john.doe@dev.local'),
    ('Jane Smith Dev', 'jane.smith@dev.local'),
    ('Bob Johnson Dev', 'bob.johnson@dev.local'),
    ('Alice Brown Dev', 'alice.brown@dev.local'),
    ('Charlie Wilson Dev', 'charlie.wilson@dev.local');

-- Insert admin user for development
INSERT INTO users (name, email) VALUES
    ('Dev Admin', 'admin@dev.local');
V3__Insert_dev_test_data.sql

Step 7.3. /stage Scripts (For QA and UAT)

This folder is for the staging or User Acceptance Testing environment. The data here is typically more realistic and curated for testers.

  • V3__Insert_stage_test_data.sql: This script inserts a smaller, more specific set of test data (e.g., ‘Stage Test User 1’, ‘Stage QA User’) suitable for the QA team to validate new features.
-- Insert staging test data
INSERT INTO users (name, email) VALUES
    ('Stage Test User 1', 'testuser1@staging.com'),
    ('Stage Test User 2', 'testuser2@staging.com'),
    ('Stage QA User', 'qa@staging.com');

-- Insert stage admin
INSERT INTO users (name, email) VALUES
    ('Stage Admin', 'admin@staging.com');
V3__Insert_stage_test_data.sql

Step 7.4. /prod Scripts (For Production)

This folder is for the live production environment. These scripts should never contain test data but might be used for initial data setup.

  • V3__Insert_prod_test_data.sql: In a real-world scenario, this script would insert essential seed data, such as an initial administrative user (‘Prod Admin’) or default application settings required for the system to function correctly upon first launch.
-- Insert prod test data
INSERT INTO users (name, email) VALUES
    ('Prod Test User 1', 'testuser1@prod.com'),
    ('Prod Test User 2', 'testuser2@prod.com'),
    ('Prod QA User', 'qa@prod.com');

-- Insert prod admin
INSERT INTO users (name, email) VALUES
    ('Prod Admin', 'admin@prod.com');
V3__Insert_prod_test_data.sql

Because each environment has its own V3 script, Flyway ensure that only the V3 script from the active profile’s folder is executed, preventing data from one environment from ever leaking into another.



5. Running Flyway Migrations with Spring Boot

With this setup, running migrations is as simple as starting your Spring Boot application with the desired profile activated.

5.1. To run for the dev environment

Start your application with the dev profile. You can do this via your IDE’s run configuration or from the command line: mvn spring-boot:run -Dspring-boot.run.profiles=dev

Upon startup, Flyway will:

  1. Connect to the flyway_demo_dev database.
  2. Scan db/migration/common and db/migration/dev.
  3. Apply V1, V2, and the dev-specific V3 script.

Output:


5.2. Verifying the Results

Once the application is running, send a GET request to the /api/users endpoint:

curl http://localhost:8080/api/users
Terminal

The response will be the development-specific data:

[
  {
    "id": 1,
    "name": "John Doe Dev",
    "email": "john.doe@dev.local"
  },
  {
    "id": 2,
    "name": "Jane Smith Dev",
    "email": "jane.smith@dev.local"
  },
  {
    "id": 3,
    "name": "Bob Johnson Dev",
    "email": "bob.johnson@dev.local"
  },
  {
    "id": 4,
    "name": "Alice Brown Dev",
    "email": "alice.brown@dev.local"
  },
  {
    "id": 5,
    "name": "Alice Brown Dev",
    "email": "alice.brown@dev.local123"
  },
  {
    "id": 6,
    "name": "Charlie Wilson Dev",
    "email": "charlie.wilson@dev.local"
  },
  {
    "id": 7,
    "name": "Dev Admin",
    "email": "admin@dev.local"
  }
]
API Response

Similarly, running with -Dspring-boot.run.profiles=stage or -Dspring-boot.run.profiles=prod will connect to the respective databases and insert the correct environment-specific data.

6. How It All Works Together

The entire process is designed to be seamless and automated, tightly integrating database migrations into the application’s startup lifecycle. Here is the step-by-step flow of what happens when you run the application:

  1. Developer Starts the Application with a Profile: A developer decides to run the application for a specific environment. They start the application from their IDE or the terminal, activating a Spring Profile, for instance: mvn spring-boot:run -Dspring-boot.run.profiles=stage.
  2. Spring Profile Activation: The -Dspring-boot.run.profiles=stage argument tells Spring Boot to activate the “stage” profile. This instructs Spring to load not only the base application.yml but also the profile-specific application-stage.yml file.
  3. Spring Configuration is Loaded: Spring merges the properties from both YAML files. The settings in application-stage.yml (like the database URL and Flyway locations) will override any defaults set in the base application.yml. The application now knows to connect to the staging database and that it should look for migration scripts in the common and stage folders.
  4. Automated Database Migration on Startup: As the Spring application context initializes, the following happens automatically:
    • Spring Boot’s auto-configuration detects that flyway-core is on the classpath.
    • It creates and configures a Flyway bean using the properties from the loaded YAML files (datasource, script locations, etc.).
    • Crucially, before initializing Hibernate or the rest of the application, Spring Boot calls the migrate() method on the Flyway bean.
    • Flyway connects to the staging database, scans the classpath:db/migration/common and classpath:db/migration/stage folders, checks its flyway_schema_history table, and applies any pending SQL scripts in version order.
  5. Application Initialization Continues: Once the Flyway migration is successfully completed, the Spring Boot startup process continues. Now, Hibernate initializes. Because of our jpa.hibernate.ddl-auto: validate setting, Hibernate connects to the now-up-to-date database and validates that the User entity’s structure matches the users table schema. If they match, the startup proceeds.
  6. Application is Ready: The application finishes starting up and is now running with a database schema that is perfectly in sync with its code.
  7. API Request and Verification: When a request hits the /api/users endpoint, the UserController queries the database via the UserRepository. It successfully retrieves and returns the data that was inserted by the combination of common and stage migration scripts, confirming that the entire automated process worked as intended.


7. Flyway Info via Actuator

While the flyway:info command is excellent for manual checks, Spring Boot provides a powerful, integrated way to monitor your database migrations via the Actuator module. By enabling the /flyway endpoint, you can get a detailed report on the status of all migrations directly from a running application.

This is incredibly useful for:

  • Automated Health Checks: A CI/CD pipeline can call this endpoint after deployment to verify that all migrations ran successfully.
  • Live Diagnostics: Quickly check the schema version of any running application instance without needing shell access.
  • Auditing: See exactly when and in what order migrations were applied.

To use it, first ensure you have the spring-boot-starter-actuator dependency and have exposed the flyway endpoint in your application.yml:

...

management:
  endpoints:
    web:
      exposure:
        include: health,flyway,metrics
...
application.yaml

You can then view the migration report by sending a GET request to the endpoint:

curl http://localhost:8080/actuator/flyway
Terminal

The response provides a comprehensive list of all migrations Flyway is aware of, including their current state.

{
    "contexts": {
        "flyway-spring-boot": {
            "flywayBeans": {
                "flyway": {
                    "migrations": [
                        {
                            "type": "SQL",
                            "checksum": 853265263,
                            "version": "1",
                            "description": "Create users table",
                            "script": "V1__Create_users_table.sql",
                            "state": "SUCCESS",
                            "installedBy": "postgres",
                            "installedOn": "2025-09-26T19:31:43.111Z",
                            "installedRank": 1,
                            "executionTime": 3
                        },
                        {
                            "type": "SQL",
                            "checksum": -55309984,
                            "version": "2",
                            "description": "Add email column",
                            "script": "V2__Add_email_column.sql",
                            "state": "SUCCESS",
                            "installedBy": "postgres",
                            "installedOn": "2025-09-26T19:31:43.111Z",
                            "installedRank": 2,
                            "executionTime": 2
                        },
                        {
                            "type": "SQL",
                            "checksum": 348766758,
                            "version": "3",
                            "description": "Insert dev test data",
                            "script": "V3__Insert_dev_test_data.sql",
                            "state": "SUCCESS",
                            "installedBy": "postgres",
                            "installedOn": "2025-09-26T19:31:43.111Z",
                            "installedRank": 3,
                            "executionTime": 2
                        }
                    ]
                }
            }
        }
    }
}
Output

As you can see, the output clearly shows that all three migrations have a state of SUCCESS. This makes the /flyway actuator endpoint an invaluable tool for managing and monitoring your database’s evolution in a live environment.



8. Advanced Control: Programmatic Access to the Flyway Bean

While Spring Boot’s “migrate on startup” behavior is perfect for most use cases, there are scenarios where you might need more direct control over Flyway from within your application’s logic. For example, you might want to build an internal admin dashboard that can:

  • Display the current migration status.
  • Trigger a migration on-demand via a secure API call.
  • Perform a repair operation in an emergency.

Spring Boot makes this possible by creating a fully configured Flyway bean in the application context. You can inject this bean directly into any of your own components (like a controller) to access its full API.

The FlywayService class below is a perfect example of this. It acts as a dedicated service layer that wraps the core Flyway commands, making them available to the rest of your application in a clean and reusable way.

package com.bootcamptoprod.service;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.MigrationInfo;
import org.flywaydb.core.api.MigrationInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class FlywayService {

    @Autowired
    private Flyway flyway;

    public void runFlywayMigrations() {
        try {
            flyway.migrate();
            System.out.println("Migrations scripts executed successfully");
        } catch (FlywayException e) {
            System.err.println("Migration failed: " + e.getMessage());
        }
    }

    public void getFlywayInfo() {
        MigrationInfoService infoService = flyway.info();
        MigrationInfo[] migrations = infoService.all();

        for (MigrationInfo migration : migrations) {
            System.out.println("Version: " + migration.getVersion());
            System.out.println("Description: " + migration.getDescription());
            System.out.println("State: " + migration.getState());
            System.out.println("---");
        }
    }

    public void validateMigrations() {
        try {
            flyway.validate();
            System.out.println("All migrations validated successfully");
        } catch (FlywayException e) {
            System.err.println("Migration validation failed: " + e.getMessage());
        }
    }

    // Note: Use with extreme caution
    public void repairIfNeeded() {
        try {
            flyway.repair();
            System.out.println("Flyway repair completed");
        } catch (FlywayException e) {
            System.err.println("Flyway repair failed: " + e.getMessage());
        }
    }

    // Note: Use with extreme caution
    public void cleanDatabase() {
        try {
            flyway.clean();
            System.out.println("Database cleaned successfully");
        } catch (FlywayException e) {
            System.err.println("Database clean failed: " + e.getMessage());
        }
    }
}
FlywayService.java

Explanation:

  • @Service: This annotation registers FlywayService as a Spring-managed bean, allowing it to be injected elsewhere in your application.
  • @Autowired private Flyway flyway: This is the key to the entire pattern. Spring’s dependency injection provides us with the singleton Flyway instance that was created and configured by the auto-configuration process. This bean already knows the correct datasource, migration locations, and all other settings from your application.yml files.
  • runFlywayMigrations(): This method calls flyway.migrate() to programmatically scan for and apply any pending database migrations from the configured script locations.
  • getFlywayInfo(): This provides a programmatic way to get the same information as the /actuator/flyway endpoint. It iterates through all migrations and prints their status, making it useful for custom logging or building a detailed response for an admin API.
  • validateMigrations(): This method executes flyway.validate(), which is useful for creating a custom health check that verifies the integrity of the applied migrations at runtime.
  • repairIfNeeded(): This method exposes the powerful but dangerous flyway.repair() command. It should be used with extreme caution and secured properly, as it directly modifies the flyway_schema_history table and is intended for recovering from failed migrations.
  • cleanDatabase(): This method programmatically executes the flyway.clean() command, a highly destructive operation that drops all objects (tables, views, etc.) from the configured schemas. Its purpose is to completely reset a database for a fresh start, which is typically only useful in development or automated testing environments. This results in irreversible data loss and should be handled with extreme caution.

9. Advanced Control: Customizing Startup Behavior with FlywayMigrationStrategy

By default, Spring Boot simply executes flyway.migrate() on application startup. However, the framework provides a powerful hook for scenarios that require more complex logic: the FlywayMigrationStrategy interface.

By defining a Spring bean of this type, you can completely override the default startup behavior and replace it with your own custom sequence of Flyway commands. This gives you fine-grained control over the migration process, allowing you to run validation, logging, or even cleaning operations before or after the main migration.

A powerful example of this is a strategy that performs a full “Clean, Info, and Migrate” flow. This is particularly useful in testing or specific development scenarios where you need to guarantee a fresh database state on every application start, complete with detailed logging.

The “Clean, Info, and Migrate” Strategy Flow:

This example strategy executes the following steps in order on every application startup:

  1. Wipe the Database (clean): It first drops all objects in the database, ensuring a completely clean slate.
  2. Log Pre-Migration State (info): It then captures and logs the status of all available migrations, which will all be in a PENDING state.
  3. Apply All Migrations (migrate): It runs the standard migration process, applying all available scripts sequentially.
  4. Log Post-Migration State (info): Finally, it logs the status again, providing a clear confirmation that all migrations now have a SUCCESS state.
package com.bootcamptoprod.strategy;

import org.flywaydb.core.api.MigrationInfo;
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.text.SimpleDateFormat;
import java.util.Arrays;

@Configuration
public class FlywayCustomMigrateStrategy {
    @Bean
    public FlywayMigrationStrategy customMigrationStrategy() {
        return flyway -> {
            // Step 1: Clean the database
            flyway.clean();

            // Step 2: Log the pre-migration state
            System.out.println("--- FLYWAY PRE-MIGRATION INFO ---");
            printMigrationTable(flyway.info().all());

            // Step 3: Run the migrations
            flyway.migrate();

            // Step 4: Log the post-migration state
            System.out.println("\n--- FLYWAY POST-MIGRATION INFO ---");
            printMigrationTable(flyway.info().all());
        };
    }

    /**
     * Pretty-prints Flyway migrations in a table format similar to Flyway CLI.
     */
    private void printMigrationTable(MigrationInfo[] migrations) {
        // Column widths
        int wCategory = 11;
        int wVersion = 9;
        int wDescription = 24;
        int wType = 8;
        int wInstalledOn = 20;
        int wState = 10;

        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

        // Border line
        String border = "+"
                + "-".repeat(wCategory + 2) + "+"
                + "-".repeat(wVersion + 2) + "+"
                + "-".repeat(wDescription + 2) + "+"
                + "-".repeat(wType + 2) + "+"
                + "-".repeat(wInstalledOn + 2) + "+"
                + "-".repeat(wState + 2) + "+";

        // Print header
        System.out.println(border);
        System.out.printf("| %-" + wCategory + "s | %-" + wVersion + "s | %-" + wDescription + "s | %-" + wType + "s | %-" + wInstalledOn + "s | %-" + wState + "s |%n",
                "Category", "Version", "Description", "Type", "Installed On", "State");
        System.out.println(border);

        // Print rows
        Arrays.stream(migrations).forEach(migrationInfo -> {
            String installedOn = migrationInfo.getInstalledOn() != null
                    ? df.format(migrationInfo.getInstalledOn())
                    : "";

            System.out.printf("| %-" + wCategory + "s | %-" + wVersion + "s | %-" + wDescription + "s | %-" + wType + "s | %-" + wInstalledOn + "s | %-" + wState + "s |%n",
                    migrationInfo.isVersioned() ? "Versioned" : "Repeatable",
                    migrationInfo.getVersion() != null ? migrationInfo.getVersion().toString() : "",
                    truncate(migrationInfo.getDescription(), wDescription),
                    migrationInfo.getType(),
                    installedOn,
                    migrationInfo.getState());
        });

        System.out.println(border);
    }

    private static String truncate(String str, int maxLen) {
        if (str == null) return "";
        return str.length() <= maxLen ? str : str.substring(0, maxLen - 3) + "...";
    }
}
FlywayCustomMigrateStrategy.java

Explanation:

  • @Configuration: This annotation marks the class as a source of bean definitions, telling Spring to scan it for beans to add to the application context.
  • @Bean public FlywayMigrationStrategy customMigrationStrategy(): This method defines our custom strategy bean. When Spring Boot starts, it detects a bean of this specific type (FlywayMigrationStrategy) and chooses to execute its logic instead of the default behavior. The lambda expression flyway -> { ... } contains our custom sequence of commands, which Spring executes with a fully configured Flyway instance.
  • printMigrationTable(…): This is a private utility method designed to enhance the developer experience. It formats the migration status into a clean, readable table in the application console, similar to the output of the Flyway command-line tool. This makes it very easy to compare the “before” and “after” states of the database during startup.

Output:



10. Source Code

The complete source code for this Flyway Spring Boot integration project is available on GitHub. The repository includes all the configuration files, migration scripts, Spring profiles, and complete Spring Boot application code demonstrated in this tutorial. Simply clone the repository, configure your PostgreSQL database connections in the application YAML files, and run the Spring Boot application with different profiles to see the multi-environment migration system in action.

🔗 Flyway with Spring Boot: https://github.com/BootcampToProd/flyway-spring-boot

11. Things to Consider

When implementing Flyway migrations using Spring Boot’s auto-configuration, it’s crucial to keep these factors in mind to ensure a robust and safe deployment process, especially in production.

  1. Migration File Immutability: Once a migration has been applied to any environment, never modify the existing migration files. Always create new versioned migrations to maintain checksum integrity and avoid validation failures across environments.
  2. Startup Dependencies: Since Spring Boot runs migrations automatically on startup, ensure your application startup process accounts for potentially long-running migrations. Consider implementing startup probes in Kubernetes or health checks in load balancers to prevent premature traffic routing.
  3. Hibernate and Flyway Conflicts: Always set spring.jpa.hibernate.ddl-auto to validate or none. Using create, create-drop, or update will cause Hibernate to fight with Flyway for control over the database schema, leading to unpredictable behavior and potential data loss. validate is a great safety net, as it confirms your JPA entities match the Flyway-managed schema.
  4. Transaction Management: Be aware that Flyway runs each migration in its own transaction by default. For complex migrations requiring multiple operations, design your scripts carefully to handle partial failures and ensure data consistency.
  5. Rollback Strategy: While Spring Boot Flyway doesn’t support automatic rollbacks, maintain comprehensive backup procedures and document manual rollback processes for each migration, especially those involving schema changes or data transformations.


12. FAQs

How does Spring Boot Flyway differ from using the Maven plugin approach?

Can I disable automatic migrations in production?

How do I handle migration failures in production?

Can I use this approach with databases other than PostgreSQL?

How do I start using Flyway on an existing database that already has data?

13. Conclusion

Flyway migration using Spring Boot represents the most streamlined approach to database schema management for Spring-based applications, combining the robustness of Flyway’s migration capabilities with Spring Boot’s developer-friendly auto-configuration. This integration eliminates the complexity of external tooling while providing enterprise-grade database evolution management through environment-specific configurations and comprehensive monitoring capabilities. The automatic migration execution during application startup, combined with Spring Actuator endpoints for monitoring and health checks, creates a seamless development experience that scales from local development to production deployments, making it the preferred choice for teams seeking reliable, maintainable database migration workflows.



14. Learn More

#

Interested in learning more?

Flyway Spring Boot Maven: Complete Guide for Multi-Environment Database Migrations



Add a Comment

Your email address will not be published.