Flyway is a powerful migration tool typically used with Java applications. This blog post aims to explain the concepts behind it in a quick and understandable way. It covers why it is needed, a simplified explanation of how it works, and some useful concepts for working with it. It focuses on the concepts and is intended to be most useful when contributing to a project where everything is already set up. How-to and setup guides can be found on Baeldung Flyway Tutorial or by asking an LLM.
A database migration is essentially just a SQL file that is executed before an application starts. The idea is that the migration files live as part of the source code, allowing you to migrate databases just before or at the same time as you upgrade the application version. Having the migrations be part of the source also makes it easier to go back to an earlier version of the application and spin up a database as it was when that version existed.
Flyway migrations
Flyway migrations usually exist in the resource folder of the database module of the application. They are usually executed in one of two ways:
- As part of application startup, where the Flyway process is embedded as part of the application.
- As a separate task that must be run before starting the application.
The first is usually preferred, but the second is sometimes required to ensure proper setup. For example, if you base your database on a previous dump of data, you might never want to start up the wrong version.
Each Flyway file follows the format <Prefix><Version>__<Name>.sql:
<Prefix>is eitherVfor a one-time migration orRfor repeatable migrations.- It is possible to configure a different prefix, but it is rarely worth the trouble.
<Version>is free-form text used only to determine the ordering of migrations.<Name>is free-form text that can be anything; it is useful for describing what the migration is about.
Note the 2 underscores between <Version> and <Name>. This can easily cost a couple of hours of debugging the first time you forget one.
Flyway files are always executed as a transaction, so there is no need to manually handle transactions (for example BEGIN and COMMIT) inside them — despite what many LLMs tend to recommend.
The main Flyway command is called migrate, and it is what you will usually run to apply all migrations to the database.
Ordering of migrations
The <Version> part of the filename specifies the ordering of migrations. Each version string must be unique across all migration files, so it is important to minimise overlap. Here are a few useful patterns:
- Incrementing integer:
1,2,3…V1__initial.sql,V2__updated_column.sql,V3__added_table.sql…- This is the simpler structure, but it is hard to prevent overlaps in projects with multiple contributors. Recommended when fewer than five people are actively contributing.
- Date and incrementing integer:
2026_01_01_1,2026_01_01_2,2026_01_01_3…V2026_01_01_1__initial.sql,V2026_01_01_2__updated_column.sql,V2026_01_01_3__added_table.sql…- Easier to keep unique when fewer migrations happen each day. Good for slow-moving projects.
- Date-time format:
2026_01_01_08_00,2026_01_01_08_01,2026_01_01_08_02…V2026_01_01_08_00__initial.sql,V2026_01_01_08_01__updated_column.sql,V2026_01_01_08_02__added_table.sql…- The most granular format; very good for large projects as collisions are rare. The downside is that you may encounter more out-of-order problems.
Two special versions exist: beforeMigrate.sql and afterMigrate.sql, which do what their name suggests. They are run before and after a migrate command.
Ordering matters because the SQL files do not necessarily produce the same result depending on when they are run. For example, consider these two migration files:
V1__add_3_rows_to_table.sql— adds 3 new rows to the tableV2__change_all_rows_of_table.sql— updates a column value on all rows
When V1 runs first, the 3 new rows will also be updated by V2. However, if V2 ran first, the 3 rows added later would not have the updated value.
Failing migration files (repair)
A Flyway file will fail if the SQL is not executable. In those cases, the database must either be updated manually or the migration file must be fixed. During development you can simply change the content and try again. However, if the migration has already been successfully executed in an environment, you should not change it. If you are forced to change it, see the repair section below.
When you are forced to change an already-applied file, other environments will encounter a checksum mismatch because the file content no longer matches what was applied. The repair command can be used in this case. Note that repair is a separate command from migrate and does not run any migrations — it only updates the stored checksum to match the new file. This means changes will not be applied to that environment. Therefore, changes to existing files should not introduce anything new; they should only fix whatever is needed for the file to be executable.
repair can also be used if you remove a previously added migration — it will remove the corresponding entry from the history table. Importantly, it will not undo the changes made by the removed migration. Migrations should never be removed (see the repeatable migrations section for edge cases).
Out of order
With some versioning strategies, an error can occur when you add a migration with a version number earlier than a migration that has already been applied. For example, suppose you added:
V2026_01_01_10_00__my_change.sql
but your colleague merged first with:
V2026_01_01_10_45__colleague_change.sql
The database may already contain your colleague’s migration, so yours would come earlier in the ordering.
Flyway has a flag for this situation: out-of-order. Enabling it allows migrations to run even if their version is earlier than the latest applied version. You should manually verify that running them out of order does not cause problems.
Note that out-of-order is a flag (used with the migrate command), whereas repair is a standalone command.
On large projects with many daily migrations, you may want to enable out-of-order by default in development environments. However, always remember to disable it in production, as applying migrations out of order can cause the issues illustrated in the example above.
Repeatable migrations
Repeatable migrations use the prefix R, for example R__my_test_users.sql. They can be used to insert test data or verify data integrity, and they always run after all standard versioned migrations.
Repeatable migrations are not run every time. Once executed, they will only run again if their content changes.
Repeatable migrations are not easily deletable. Because their execution is recorded in the history table, a repair is needed if you remove one. If you want to deactivate a repeatable migration, one option is to empty the file or add a comment explaining why it is no longer needed.
If you use a repeatable migration to check data integrity, you can add ${flyway:timestamp} at the top of the file. This inserts the current execution time, which forces Flyway to always re-run the migration since the content appears to change each time.
How does Flyway do it?
Flyway creates a table called flyway_schema_history in your database. This is a normal table that can be queried by any SQL user. It stores the prefix, version, name, and checksum of every migration that has been applied. When Flyway runs, it compares the migration files in your codebase against this history and executes any migrations that are not yet present.
Out-of-order errors occur when a row in that table contains a later version than a migration that has not yet been run.
Purging migrations
In long-running projects, the list of migrations can become very long. Running all of them in order to recreate a database from scratch can take a significant amount of time, which can be a problem in local development. A good recommendation is to create a dump of a fully migrated database that you can use locally to save time.
If you want to remove old migrations entirely, the process is as follows:
- Use a database tool to generate a full SQL schema recreation of the current database.
- Remove all existing Flyway migrations and add a single new migration containing the generated schema.
- Manually clear the
flyway_schema_historytable and insert a fake entry for the new migration, since the schema is already in place.
This process is risky because it requires a manual step. Additionally, if you have a partially migrated database, you may end up with a schema that differs from the new baseline. This approach is useful before a system goes to production but should never be done afterwards.
Conclusion
The aim of this post is to explain the core concepts behind Flyway. Reach out if you think anything important is missing.