Modern Spring Boot applications rarely remain simple for long. A project may begin with a few entities and a handful of tables, but as it evolves, database schemas change continuously: columns are added, indexes are introduced, constraints change, new tables appear, and sometimes data itself needs to be transformed.
The challenge isn't creating these changes. The challenge is making sure the same changes are applied consistently across every environment.
This is where Flyway comes in.
Flyway provides a versioned, repeatable mechanism for evolving a relational database schema alongside application code. When integrated with Spring Boot, it allows database changes to become part of the application's lifecycle and, eventually, the CI/CD deployment pipeline.
In this article, we'll explore Flyway from the ground up and build a Spring Boot + PostgreSQL setup using migrations.
1. The Problem: Database Schema Changes
Imagine a Spring Boot application initially has:
users
projectsThe application reaches production.
A few weeks later, we need to add:
ALTER TABLE users
ADD COLUMN email VARCHAR(255);Then another developer adds:
CREATE INDEX idx_users_email
ON users(email);And another release introduces:
CREATE TABLE project_members (...);Now we have multiple environments:
Developer A
Database → Version 5
Developer B
Database → Version 4
QA
Database → Version 3
Production
Database → Version 2Someone has to know:
Which SQL scripts have already run?
Which scripts still need to run?
In what order?
Has production received the same schema changes as QA?
Can a new developer reproduce the database from scratch?
Can CI/CD safely upgrade an existing database?
Without a migration mechanism, database changes often become a collection of manually executed SQL scripts and deployment instructions.
That doesn't scale well.
2. What Is Flyway?
Flyway is a database migration and versioning tool.
Its fundamental idea is simple:
Treat database schema changes as version-controlled artifacts.
Instead of manually changing a database, we create migration files:
V1__initial_schema.sql
V2__create_users.sql
V3__create_projects.sql
V4__add_email_to_users.sql
V5__create_project_members.sqlFlyway executes them in order.
Conceptually:
Git Repository
│
▼
Database Migrations
│
┌───────────┼───────────┐
▼ ▼ ▼
V1 V2 V3
│ │ │
└───────────┴───────────┘
│
▼
PostgreSQLThe important part is that the migration files live alongside your application code.
Therefore:
Application version
+
Database schema versioncan evolve together.
3. Flyway's Migration Philosophy
Flyway follows a straightforward principle:
Don't manually modify a managed database schema. Create a migration instead.
For example, suppose V2 created:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL
);Later, we need an email address.
Don't edit V2.
Instead, create V3:
ALTER TABLE users
ADD COLUMN email VARCHAR(255);So the history becomes:
V1
│
▼
Initial database
│
▼
V2
│
▼
users table
│
▼
V3
│
▼
users.emailThis historical record is extremely valuable.
4. Flyway Migration Naming
Flyway's conventional versioned migration naming pattern is:
V<version>__<description>.sqlFor example:
V1__initial_schema.sql
V2__create_users.sql
V3__create_projects.sql
V4__add_email_to_users.sqlNotice the double underscore:
V1__initial_schema.sql
^^The first part is the migration version.
The second part is the human-readable description.
5. How Flyway Knows What Has Already Run
This is one of Flyway's most important features.
Flyway creates a metadata table in the database:
flyway_schema_historyIt records migrations that have been executed.
Conceptually, it looks like:
| Version | Description | Success |
|---|---|---|
| 1 | initial schema | true |
| 2 | create users | true |
| 3 | create projects | true |
| 4 | add email | true |
When the application starts, Flyway compares:
Migration files in application
↓
flyway_schema_history in databaseFor example:
Application:
V1
V2
V3
V4
V5
Database:
V1
V2
V3
V4Flyway recognizes:
V1 → already applied
V2 → already applied
V3 → already applied
V4 → already applied
V5 → pendingIt executes V5.
6. Flyway and Spring Boot
Spring Boot makes Flyway integration relatively straightforward.
A typical Gradle setup includes:
implementation("org.springframework.boot:spring-boot-starter-flyway")
implementation("org.flywaydb:flyway-database-postgresql")The first dependency integrates Flyway with Spring Boot.
The second provides PostgreSQL-specific Flyway support.
This distinction is particularly important with modern Flyway versions, where database support is modularized.
For PostgreSQL:
Spring Boot
│
▼
spring-boot-starter-flyway
│
├── Flyway Core
│
└── PostgreSQL database module
│
▼
PostgreSQL7. Spring Boot Configuration
A typical application.yml might contain:
spring:
application:
name: astra
datasource:
url: jdbc:postgresql://localhost:5432/astra
username: astra
password: astra_local
jpa:
hibernate:
ddl-auto: validateThe important configuration here is:
spring:
jpa:
hibernate:
ddl-auto: validateThis creates an important separation of responsibilities.
Flyway
Owns:
Database schema evolutionHibernate
Owns:
Object-relational mappingHibernate should not be responsible for silently modifying the production schema.
8. Why ddl-auto=update Isn't My Preferred Production Strategy
You may see Spring Boot examples using:
spring:
jpa:
hibernate:
ddl-auto: updateThis can be convenient during experimentation.
Hibernate can attempt to modify the database based on changes to entities.
For example:
@Entity
public class User {
private Long id;
private String username;
private String email;
}Hibernate may infer that the database needs an additional column.
That sounds convenient, but it becomes problematic when database changes need to be:
reviewed
audited
tested
rolled through environments
coordinated with application releases
reproduced reliably
For a production-oriented architecture, I prefer:
ddl-auto: validateand:
Flyway → schema changes
Hibernate → entity mapping9. The Flyway + Hibernate Responsibility Boundary
This distinction is worth remembering.
Flyway asks:
"What does the database schema look like at version N?"
Hibernate asks:
"How does my Java domain model map onto that schema?"
For example:
PostgreSQL
▲
│
┌───────┴───────┐
│ │
Flyway Hibernate
│ │
│ │
Schema evolution ORM mapping
│ │
└───────┬───────┘
│
Spring BootThis separation makes the architecture much easier to reason about.
10. Creating the First Migration
The conventional location in a Spring Boot project is:
src/
└── main/
└── resources/
└── db/
└── migration/So:
src/main/resources/db/migration/A migration could be:
V1__initial_schema.sqlFor example:
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);When the application starts, Flyway discovers the migration.
11. What Happens During Application Startup?
A simplified startup sequence looks like:
Spring Boot starts
│
▼
Create DataSource
│
▼
Connect to PostgreSQL
│
▼
Initialize Flyway
│
▼
Check flyway_schema_history
│
▼
Find pending migrations
│
▼
Execute migrations
│
▼
Initialize JPA/Hibernate
│
▼
Application readyThis ordering is important.
We want the database schema to be ready before Hibernate begins relying on it.
12. What Happens When a Migration Fails?
Suppose:
V1 ✅
V2 ✅
V3 ❌Flyway records the migration state, and the application startup fails rather than silently continuing with an unexpected schema.
That's generally desirable.
You don't want:
Application:
"Database migration failed, but let's start anyway."You want:
Migration failed
↓
Application startup fails
↓
Deployment fails
↓
Problem gets noticedThis is an important fail-fast principle.
13. Flyway in CI/CD
This is where Flyway becomes particularly powerful.
Imagine Astra has:
Developer
↓
Git
↓
CI
↓
Tests
↓
Build
↓
Migration
↓
DeploymentA deployment pipeline might conceptually look like:
Git
│
▼
Build Astra
│
▼
Run Tests
│
▼
Build Artifact
│
▼
Run Flyway Migration
│
▼
Deploy ApplicationNow the database isn't an independent manual task.
It becomes part of the software delivery lifecycle.
14. Flyway Across Multiple Environments
Suppose Astra has:
Development
QA
UAT
ProductionEach database may be at a different migration version.
For example:
Development → V10
QA → V9
UAT → V8
Production → V7When V10 is deployed to production, Flyway can apply:
V8
V9
V10in order.
This gives us an important property:
Database environments can converge toward the same schema version through controlled migrations.
15. Database Migrations Should Be Immutable
Once a migration has been applied to a shared environment, don't casually edit it.
Suppose:
V3__create_projects.sqlhas already been executed.
Don't modify V3 to add a new column.
Instead:
V4__add_project_status.sqlThis preserves the history:
V1
V2
V3
V4rather than rewriting history.
This is very similar to the principle of immutable commits in source control.
16. Example: Evolving Astra
Let's imagine Astra eventually has:
projectsV1
CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);V2
We introduce a status:
ALTER TABLE projects
ADD COLUMN status VARCHAR(30) NOT NULL DEFAULT 'ACTIVE';V3
We add an index:
CREATE INDEX idx_projects_status
ON projects(status);The history becomes:
V1 → projects
V2 → project status
V3 → project status indexA new developer can reconstruct the database by running all migrations from V1 onward.
17. Schema Changes vs Data Migrations
Not every migration is simply:
CREATE TABLESometimes data needs to be transformed.
For example:
Old:
full_name
New:
first_name
last_nameA migration might need to:
Add new columns.
Transform existing data.
Validate the transformation.
Eventually remove the old column.
That migration might involve:
ALTER TABLE users
ADD COLUMN first_name VARCHAR(100);
ALTER TABLE users
ADD COLUMN last_name VARCHAR(100);
UPDATE users
SET ...These migrations require significantly more care because they operate on existing production data, not merely schema metadata.
18. Backward-Compatible Database Changes
One of the most important production considerations is that application deployment and database migration don't always happen instantaneously.
Suppose version 1 of the application expects:
users.nameand version 2 expects:
users.first_name
users.last_nameImmediately renaming the column could break the currently running version.
A safer deployment strategy may be:
Phase 1
Add new columns
↓
Phase 2
Application starts writing both representations
↓
Phase 3
Backfill existing data
↓
Phase 4
Application reads new columns
↓
Phase 5
Remove old columnThis is often called an expand-and-contract approach.
Flyway provides the mechanism for versioning those database changes; the deployment strategy determines how safely the application and database evolve together.
19. Flyway and Docker
Flyway works particularly well with containerized applications.
For local Astra development:
Docker
│
└── PostgreSQLwhile:
Astra Spring Boot
│
└── Flyway
│
└── PostgreSQLYour local database can therefore be recreated while your migration history remains in Git.
That is extremely useful for onboarding:
git clone Astra
↓
docker compose up
↓
./gradlew bootRun
↓
Flyway
↓
Database automatically reaches required schemaA new developer doesn't need a 20-page database setup document.
20. Flyway and Azure
This becomes even more interesting for an Azure-based architecture.
Astra could eventually look like:
Azure
│
┌──────┴──────┐
│ │
Astra API PostgreSQL
│ │
└──────┬──────┘
│
FlywayThe local environment might use:
Docker PostgreSQLwhile Azure uses:
Azure Database for PostgreSQLThe migration files remain the same:
V1
V2
V3
...Only the database connection changes.
That is one of the major benefits of treating migrations as application artifacts rather than manually maintained infrastructure instructions.
21. Secrets Shouldn't Live in application.yml
For local experimentation, you might have:
spring:
datasource:
username: astra
password: astra_localBut production credentials should not be committed into Git.
A more appropriate approach is environment-driven configuration:
spring:
datasource:
url: ${DB_URL}
username: ${DB_USERNAME}
password: ${DB_PASSWORD}Then production can provide those values through an appropriate secret-management mechanism.
In Azure, this can eventually be integrated with services such as Azure Key Vault.
The important principle is:
Configuration ≠ Secret22. A Practical Project Structure
A mature Spring Boot application might look like:
backend/
├── build.gradle.kts
├── gradlew
├── gradlew.bat
│
└── src/
├── main/
│ ├── java/
│ │ └── com/rishiraj/astra/
│ │
│ └── resources/
│ ├── application.yml
│ │
│ └── db/
│ └── migration/
│ ├── V1__initial_schema.sql
│ ├── V2__create_users.sql
│ └── V3__create_projects.sql
│
└── test/The important relationship is:
Java code
+
Migration scripts
+
Application configuration
+
Tests
↓
Version controlled together23. Common Mistakes
Mistake 1 — Editing old migrations
Avoid:
V2__create_users.sqlbeing changed after V2 has been applied elsewhere.
Create:
V3__modify_users.sqlinstead.
Mistake 2 — Letting Hibernate own production schema changes
Avoid relying on:
ddl-auto: updateas the production migration mechanism.
Use Flyway for explicit schema evolution.
Mistake 3 — Manually changing production
Avoid:
SSH into production
↓
psql
↓
ALTER TABLE...unless there's an exceptional operational reason.
If it's a planned schema change, preferably make it a migration.
Mistake 4 — Putting credentials in Git
Avoid:
password: production-secretin a committed configuration file.
Use secret management.
Mistake 5 — Making huge migrations
Prefer small, understandable changes:
V5 → Add project status
V6 → Add status index
V7 → Add project ownerrather than one enormous migration containing unrelated changes.
24. Flyway vs Hibernate
A useful comparison:
| Responsibility | Flyway | Hibernate |
|---|---|---|
| Create schema | ✅ | Possible, but not preferred for production |
| Version schema | ✅ | ❌ |
| Track migrations | ✅ | ❌ |
| Execute SQL migrations | ✅ | ❌ |
| ORM | ❌ | ✅ |
| Entity mapping | ❌ | ✅ |
| JPQL | ❌ | ✅ |
| Schema validation | ❌ | ✅ |
| Production schema lifecycle | ✅ | Usually not its role |
The two tools complement each other.
They aren't competitors.
25. The Mental Model I Recommend
For a production-oriented Spring Boot system, think:
APPLICATION
│
┌─────────┴─────────┐
│ │
Hibernate Flyway
│ │
│ │
Object Mapping Schema Evolution
│ │
└─────────┬─────────┘
│
▼
PostgreSQLHibernate knows:
What is a
Project?
Flyway knows:
How did the database become capable of storing a
Project?
That distinction is subtle but extremely important.
26. Where Astra Stands Now
For our Astra backend, we've established:
Java 25
↓
Spring Boot 4.1
↓
Gradle
↓
Spring Data JPA
↓
Hibernate
↓
HikariCP
↓
PostgreSQL 17And we've now added:
Flyway
↓
Versioned database migrationsSo the architecture is becoming:
Astra
│
┌───────────┴───────────┐
│ │
Hibernate Flyway
│ │
Entity Mapping Schema Evolution
│ │
└───────────┬───────────┘
▼
PostgreSQLOur next important step shouldn't be another framework.
It should be domain design.
Before creating the real V1 migration, we should determine Astra's core entities, relationships, constraints, indexes, and authorization model. Then we'll translate that model into a proper migration and corresponding JPA entities.
That will turn Astra from a Spring Boot demonstration project into an architecture project.
Comments
Post a Comment