Flyway Database Migrations in Spring Boot: Why One ALTER TABLE Can Bring Down Your Service

I Fired Off an ALTER TABLE on the Production DB

Friday, 4 p.m. At a stakeholder’s request, I had to add one column to the project table. A simple job.

ALTER TABLE project ADD COLUMN priority VARCHAR(20);

I tested it on the dev server, connected to the production server, and ran the same SQL. No problems. I deployed and went home.

Monday morning, the phone rang.

“That thing deployed over the weekend works on the dev server, but production is throwing errors.”

When I looked into it, another developer had run SQL directly against the production DB on Friday evening, adding a different column to the same table. The order got tangled, and the schema the application expected no longer matched the actual DB schema. There was no record of which SQL had run when, so I had to compare the dev and production schemas by hand, one by one.

‘Never manage DB schema changes by hand.’ Learning that lesson cost me half a day.

When two developers modify the production DB by hand at the same time, nobody knows the full picture anymore.

What Is a Database Migration, and Why Is Manual SQL Dangerous?

A database migration means managing the change history of your database schema as code. Just as Git tracks the change history of your source code, a DB migration tool tracks the change history of your table structures.

Here’s why manual SQL is dangerous.

Managing SQL by handMigration tool
No idea who ran which SQL, or whenEvery change is recorded in a file
Dev and production schemas can drift apartApplied to every environment in the same order
The same SQL can get run twiceAnything already applied is skipped
Mistakes are hard to undoThe history makes problems traceable
A new team member can’t build the DB from scratchRun the migration files and it’s reproduced

The key idea: ‘turn schema changes into code and version them.’


Getting Started with Flyway

Flyway is the most widely used DB migration tool in the Java/Spring ecosystem. It’s based on SQL files, so there’s no new syntax to learn. You just write SQL.

Adding the Dependency

// build.gradle
implementation 'org.flywaydb:flyway-core'

In Spring Boot, adding this one dependency is all it takes: migrations run automatically when the application starts.

Configuration

# application.yml
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true    # when applying Flyway to an existing DB for the first time

baseline-on-migrate: true is needed when you first introduce Flyway to a production DB that already has tables. It treats the existing schema as “version 1” and tracks changes from there on.

File Naming Conventions

Migration files go in the src/main/resources/db/migration/ folder, and their names follow a convention.

V1__create_project_table.sql
V2__add_priority_column.sql
V3__create_member_table.sql
V3.1__add_member_email_index.sql
PartMeaningExample
VVersioned migrationV
1, 2, 3Version number (run in order)V2
__ (two underscores)SeparatorV2__
DescriptionWhat this migration doesadd_priority_column
.sqlSQL file.sql

Flyway runs migrations in version-number order and skips versions that have already run. The execution history is recorded in the flyway_schema_history table.

Your First Migration

-- V1__create_project_table.sql
CREATE TABLE project (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL,
    status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE',
    region VARCHAR(100),
    category VARCHAR(100),
    manager_name VARCHAR(100),
    start_date DATE,
    end_date DATE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

When you start the application, Flyway detects this file and runs it automatically. You can confirm it in the logs.

Flyway Community Edition
Successfully applied 1 migration to schema "public" (execution time 00:00.032s)
Flyway runs migration files in version order and records the history.

Real-World Patterns

Adding a Column

This is the most common migration, and the very operation that caused all the trouble in the intro.

-- V2__add_priority_to_project.sql
ALTER TABLE project ADD COLUMN priority VARCHAR(20) DEFAULT 'NORMAL';

Transforming Existing Data

After adding a column, you’ll often need to backfill existing data.

-- V3__populate_priority_from_category.sql
UPDATE project SET priority = 'HIGH' WHERE category = 'URGENT';
UPDATE project SET priority = 'NORMAL' WHERE priority IS NULL;

It’s best to keep schema changes and data transformations in separate files. Mix them in one file and it gets hard to trace where a problem came from.

Adding Indexes

-- V4__add_index_project_status.sql
CREATE INDEX idx_project_status ON project(status);
CREATE INDEX idx_project_region ON project(region);

Deployment and Migration Order

What matters in practice is ‘deployment order.’ If you deploy code that uses the new column first, it fails against a DB that doesn’t have the column yet.

Safe order:

  1. Run the migration (add the column) -> deploy the code (use the column)
  2. Or: add the column + set a default -> deploy the code -> backfill the data

Risky order:

  1. Deploy the code (use the column) -> run the migration (add the column) -> error!

With Spring Boot + Flyway, migrations run first when the application starts, so if you ship them in the same deployment, the order works out naturally. But with zero-downtime deployments (rolling deployments), there’s a moment when old code and the new schema coexist, so new columns must always have a default value or allow NULL.


More Important Than Rollback: Forward Fix

“Can I roll back a migration?”

This question comes up a lot, but here’s the reality: ‘DB migrations don’t roll back as easily as application code.’

With code, you revert to the previous version and you’re done. A database is different. If data has already gone into a column you added, dropping the column drops the data with it. If you renamed a table while another service was still accessing it by the old name, reverting doesn’t help: the errors have already happened.

That’s why, in practice, instead of preparing rollback scripts in advance, teams use a ‘Forward Fix’ strategy.

StrategyDescription
Forward FixWhen something goes wrong, fix it with a new migration (V5 fixes V4’s mistake)
Backward compatibilityDesign so old code and the new schema can coexist
BackupsBacking up the DB before a migration is a given
Small stepsDon’t make big changes all at once; split them into small steps
-- If V4 set the wrong column type by mistake,
-- don't edit V4; add V5 instead

-- V5__fix_priority_column_type.sql
ALTER TABLE project ALTER COLUMN priority TYPE VARCHAR(50);

Flyway raises a checksum error if you modify a migration file that has already run. To fix a mistake, don’t edit the existing file; add a new version file instead.


Flyway vs Liquibase

Liquibase also comes up a lot as a DB migration tool. The core difference between the two is simple.

FlywayLiquibase
Migration formatSQL filesXML/YAML/JSON/SQL
Learning curveLow (you only need SQL)High (its own syntax)
DB independenceSQL is DB-specificDB-independent if you use its own format
RollbackManual (Forward Fix)Partial support for automatic rollback
Best fitTeams comfortable with SQL, single DBTeams that must support multiple DBs

If your team uses only PostgreSQL and is comfortable with SQL, Flyway is more intuitive. The simplicity of “add one SQL file and you’re done” is Flyway’s biggest strength.


Practical Advice

1. Introducing Flyway to an Existing Project

Applying Flyway for the first time to a DB that’s already in production is the trickiest part. Treat the current schema as V1 and set baseline-on-migrate: true.

-- V1__baseline.sql
-- Records the current production schema as-is (for the record, not executed)
-- Extract the current schema with pg_dump --schema-only and paste it here

2. Team Rules

RuleWhy
Never modify a migration file once it’s committedPrevents checksum errors
Use descriptive file namesV7__add_index_project_status.sql, not V7__add_index.sql
Keep DDL and DML in separate filesMakes problems easier to trace
No manual SQL on the production DBPrevents drift from the schema Flyway manages
Back up before migratingThe most basic of basics

3. Useful Settings for Local Development

# application-local.yml
spring:
  flyway:
    clean-disabled: false   # allow clean only locally (never in production)
  jpa:
    hibernate:
      ddl-auto: validate    # Flyway manages the schema; Hibernate only validates

ddl-auto: validate matters. Hibernate won’t create or modify the schema on its own; it only checks that your entities match the schema Flyway built. If anything doesn’t match, you get an error at startup, so you find out right away.


Wrapping Up: Schema Changes Are Code Too

Plenty of teams still manage code changes with Git but manage DB changes by hand. With Flyway, you can review, track, and reproduce DB changes just like code.

To sum up:

  • Don’t run manual SQL directly on the production DB.
  • Make every schema change a migration file.
  • Don’t edit files that have already run; fix things with a new file (Forward Fix).
  • With Flyway + Spring Boot, you can get started by adding a single dependency.

In the next post, I’ll cover environment separation and Profiles. When something works on the dev server but not in production, the cause is very often a difference in configuration files. We’ll look at how to manage application-local.yml, application-dev.yml, and application-prod.yml.

Leave a Comment