Course topics

By WebNest Studio

Spring Boot Tutorial

Database Migrations with Flyway and Liquibase

Your schema evolves with your code: new tables, new columns, renamed fields, new indexes. On a developer laptop you can drop and recreate the database, but production data must be preserved and every environment must end up with exactly the same schema. Database migration tools solve this by keeping every schema change as a versioned script in source control and applying pending scripts automatically at startup.

Spring Boot integrates the two most popular tools, Flyway and Liquibase. In Spring Boot 4 you add them through dedicated starters (spring-boot-starter-flyway, spring-boot-starter-liquibase). This lesson covers both, with naming conventions, data migrations, safe changes for live systems, and testing.

How Migration Tools Work

The tool keeps a history table in your database (flyway_schema_history or DATABASECHANGELOG). At startup it compares the scripts in your project with that table and runs any new ones in order, recording each one and its checksum. Already applied scripts are never run again, and editing one is detected as a checksum mismatch. The rule is simple: never modify a migration that has been applied anywhere; add a new one.

Flyway

Flyway uses plain SQL files in src/main/resources/db/migration named V<version>__<description>.sql — for example V1__create_customers.sql, V2__add_phone_to_customers.sql. Repeatable migrations (R__refresh_views.sql) rerun whenever their content changes, useful for views and functions. Java-based migrations are available for complex data changes. Many databases (PostgreSQL, MySQL, SQL Server, Oracle) need an extra Flyway module such as flyway-database-postgresql.

Liquibase

Liquibase describes changes as changesets in YAML, XML, JSON or SQL, listed in a master changelog (db/changelog/db.changelog-master.yaml). Its database-independent change types (createTable, addColumn) let one changelog target several databases, and it supports automatic rollback for many change types. Choose Liquibase when you need multi-database support or rollbacks; choose Flyway for simplicity and plain SQL.

Hibernate and Migrations Together

Once migrations own the schema, set spring.jpa.hibernate.ddl-auto=validate. Hibernate then checks at startup that entities match the tables created by your scripts and fails fast if someone forgot a migration.

Zero-Downtime Changes (Expand and Contract)

During a rolling deployment, old and new versions of your application run against the same database at the same time. So never make a breaking change in one step. To rename a column: 1) add the new column, 2) deploy code that writes both and reads the new one, 3) backfill data, 4) deploy code that uses only the new column, 5) drop the old column in a later release. Add indexes concurrently on large PostgreSQL tables to avoid locking writes.

Examples

Flyway setup and the first migrations

Java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-flyway</artifactId>
</dependency>
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-database-postgresql</artifactId>
</dependency>

-- src/main/resources/db/migration/V1__create_customers.sql
create table customers (
    id          bigint generated by default as identity primary key,
    email       varchar(255) not null unique,
    full_name   varchar(200) not null,
    created_at  timestamp with time zone not null default now()
);

-- src/main/resources/db/migration/V2__create_orders.sql
create table orders (
    id           bigint generated by default as identity primary key,
    customer_id  bigint not null references customers(id),
    status       varchar(20) not null,
    created_at   timestamp with time zone not null default now()
);
create index idx_orders_customer on orders(customer_id);

-- src/main/resources/db/migration/V3__add_phone_to_customers.sql
alter table customers add column phone varchar(20);
Output
Flyway Community Edition by Redgate
Database: jdbc:postgresql://localhost:5432/webnest (PostgreSQL 17.2)
Successfully validated 3 migrations
Current version of schema "public": << Empty Schema >>
Migrating schema "public" to version "1 - create customers"
Migrating schema "public" to version "2 - create orders"
Migrating schema "public" to version "3 - add phone to customers"
Successfully applied 3 migrations to schema "public", now at version v3

Flyway configuration and a data migration

Java
# application.yml
spring:
  flyway:
    locations: classpath:db/migration
    baseline-on-migrate: true   # for adopting Flyway on an existing database
  jpa:
    hibernate:
      ddl-auto: validate

-- V4__split_full_name.sql : expand step, keeps the old column for now
alter table customers add column first_name varchar(100);
alter table customers add column last_name  varchar(100);

update customers
set first_name = split_part(full_name, ' ', 1),
    last_name  = nullif(substr(full_name, length(split_part(full_name, ' ', 1)) + 2), '');
Output
Migrating schema "public" to version "4 - split full name"
Successfully applied 1 migration to schema "public", now at version v4

select version, description, success from flyway_schema_history;
 1 | create customers        | t
 2 | create orders           | t
 3 | add phone to customers  | t
 4 | split full name         | t

Liquibase with a YAML changelog

Java
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-liquibase</artifactId>
</dependency>

# src/main/resources/db/changelog/db.changelog-master.yaml
databaseChangeLog:
  - include:
      file: db/changelog/001-create-courses.yaml
  - include:
      file: db/changelog/002-add-price.yaml

# db/changelog/001-create-courses.yaml
databaseChangeLog:
  - changeSet:
      id: 001-create-courses
      author: webnest
      changes:
        - createTable:
            tableName: courses
            columns:
              - column: { name: id, type: bigint, autoIncrement: true, constraints: { primaryKey: true } }
              - column: { name: slug, type: varchar(100), constraints: { nullable: false, unique: true } }
              - column: { name: title, type: varchar(200), constraints: { nullable: false } }

# db/changelog/002-add-price.yaml
databaseChangeLog:
  - changeSet:
      id: 002-add-price
      author: webnest
      changes:
        - addColumn:
            tableName: courses
            columns:
              - column: { name: price, type: decimal(10,2), defaultValueNumeric: 0 }
      rollback:
        - dropColumn: { tableName: courses, columnName: price }
Output
Liquibase: Running Changeset: db/changelog/001-create-courses.yaml::001-create-courses::webnest
Liquibase: Running Changeset: db/changelog/002-add-price.yaml::002-add-price::webnest
Liquibase: Update command completed successfully.

Testing migrations against a real database with Testcontainers

Java
@SpringBootTest
@Testcontainers
class MigrationTest {

    @Container
    @ServiceConnection
    static PostgreSQLContainer postgres = new PostgreSQLContainer("postgres:17");

    @Autowired JdbcClient jdbc;

    @Test
    void allMigrationsApplyAndSchemaMatchesEntities() {
        // Context startup already ran Flyway and Hibernate 'validate'
        Integer applied = jdbc.sql("select count(*) from flyway_schema_history where success")
            .query(Integer.class).single();
        assertThat(applied).isGreaterThanOrEqualTo(4);
    }
}
Output
MigrationTest > allMigrationsApplyAndSchemaMatchesEntities() PASSED
(Catches broken SQL and entity/schema mismatches before they reach production.)

Common Mistakes

  • Editing a migration file after it has been applied, causing "Validate failed: Migration checksum mismatch" in every other environment.
  • Keeping ddl-auto=update alongside Flyway, so Hibernate and Flyway both change the schema.
  • Making breaking changes (rename/drop column) in one step during rolling deployments.
  • In Spring Boot 4, adding only flyway-core without spring-boot-starter-flyway, so Flyway is not auto-configured.
  • Forgetting the database-specific Flyway module (e.g. flyway-database-postgresql) and getting "Unsupported Database".

Key Points to Remember

  • Migrations are versioned scripts in source control, applied automatically and recorded in a history table.
  • Flyway: V<version>__<description>.sql in db/migration; Liquibase: changesets in a master changelog.
  • In Spring Boot 4, use spring-boot-starter-flyway or spring-boot-starter-liquibase.
  • Never edit applied migrations; set ddl-auto=validate so Hibernate checks the schema.
  • Use expand-and-contract for zero-downtime schema changes and test migrations with Testcontainers.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.