What Is Flyway Schema Migration?
A ground-up guide to how teams version, track, and safely evolve a database schema over time — explained so a complete beginner can follow every step.
Imagine a house where five different contractors each hold their own copy of the blueprint, and each one is free to knock down a wall, add a room, or move a doorway whenever they feel like it — without telling anyone else. Sooner or later, someone opens a door and falls into a wall that used to be a hallway. This is exactly the chaos that happens inside a software team when many developers change a database’s structure by hand, on their own machines, with no shared record of what changed and when. Flyway exists to solve this one problem: giving every database, on every machine, a single, ordered, trustworthy history of every structural change it has ever gone through. This article walks through what that means, how Flyway does it internally, and how real engineering teams use it every day.
Foundations
ACore Concepts
Before touching Flyway itself, it helps to be completely clear on two separate ideas: what a “schema” is, and what “migrating” it actually means.
What is a database schema?
A database schema is the structural skeleton of a database: the tables it contains, the columns inside each table, the data types of those columns, the relationships (foreign keys) between tables, the indexes that speed up searches, and the constraints that keep bad data out. Think of it as the floor plan of a building. The floor plan is not the furniture or the people living inside — it is the walls, doors, and rooms that everything else depends on. A schema is the same thing, but for data: it defines the “rooms” that rows of data will eventually live in.
What is schema migration?
Schema migration is the controlled process of changing that floor plan over time — adding a new room, widening a doorway, renaming a hallway — without demolishing the house while people are still living in it. In database terms, this means adding a table, adding a column, changing a column’s type, adding an index, or removing something that is no longer needed, all while the application built on top of that schema keeps running correctly, ideally with zero data loss and minimal downtime.
Think of a shared Google Doc used by ten people to plan a wedding. If everyone edits their own private copy and emails it around, you eventually get five different guest lists, none of which agree. Google Docs solves this by keeping one single source of truth with a visible history of every edit, in order, with a name attached to each change. Flyway is that shared, ordered edit history — except the “document” is your database’s structure, and the “edits” are SQL scripts.
What is Flyway, specifically?
Flyway is an open-source database migration tool. It does not replace your database — it sits alongside it as a small command-line program (or a library embedded inside your application) whose only job is to look at a folder of migration files, compare them against a special bookkeeping table inside the database itself, figure out which changes have not yet been applied, and apply the missing ones in the correct order. It supports most major relational databases, including PostgreSQL, MySQL, Oracle, SQL Server, and many others, through a common approach and a set of database-specific adapters.
It is worth being precise about what Flyway is not, since the surrounding ecosystem contains tools that solve adjacent but different problems. Flyway is not a database itself, not a backup tool, and not a general-purpose data-transfer or ETL tool. It also does not automatically detect the difference between a live database’s current structure and some desired target structure the way certain “schema diffing” tools do — Flyway has no opinion about what your schema should eventually look like. It only knows two things: the ordered list of migration files it has been pointed at, and the ordered list of migrations already recorded as applied. Everything it does follows from comparing those two lists.
Why not just let an application framework manage the schema automatically?
Many application frameworks include a feature that can look at your code’s data model and automatically create or adjust matching database tables on startup. This feels convenient early in a project, but it becomes risky once real data exists: an automatic tool optimizing for “make the schema match the code” has no way of knowing whether a column being removed still holds data someone needs, or whether a type change might silently truncate values. Explicit, hand-written, reviewed migration files give a team a deliberate checkpoint to think through exactly those questions before a change reaches a database holding real, valuable data.
Versioned migrations vs. repeatable migrations
Flyway organizes changes into two broad categories, and understanding the difference is the single most important concept in this entire topic.
Versioned migrations are one-time, ordered changes. Each one is given a version number (for example, “1”, “2”, “2.1”, “3”) and a description, and Flyway guarantees it will run exactly once, in strict numerical order, never again, on a given database. These are used for things like creating a table, adding a column, or inserting a fixed piece of seed data.
Repeatable migrations have no version number at all. Instead, Flyway re-runs them every single time their content changes, regardless of order relative to versioned migrations. These are typically used for things that can be safely recreated from scratch, such as view definitions, stored procedures, or functions — objects where “the current definition wins” rather than “apply once, forever.”
Runs Exactly Once
Numbered in strict order. Perfect for creating tables, adding columns, or one-time data fixes. Once applied, it is locked in history and never re-run.
Runs On Every Change
No version number. Re-applied whenever its file content changes. Ideal for views, stored procedures, and functions that should always reflect the latest definition.
Reverses A Version
An optional, paid-tier migration type that reverses a specific versioned migration. Not available in every plan and not a substitute for backups.
Marks A Starting Point
A special marker used when Flyway is introduced into a database that already has tables, telling Flyway “assume everything up to this point already happened.”
Every versioned migration file follows a naming convention that Flyway parses to determine order and description — typically a prefix letter, a version number, two underscores, and a human-readable description, such as a file meaning “version 2, add a users table.” Flyway reads this filename, not the file’s creation date or its position in a folder listing, to decide what order things run in. This is deliberate: file systems and version control tools do not reliably preserve creation order once a project has multiple contributors and branches, but a version number embedded in the filename always sorts consistently.
Anatomy of a migration filename
Break a typical filename into its parts and the naming scheme stops looking arbitrary. The leading letter tells Flyway which category the file belongs to — a plain version prefix for a versioned migration, a different prefix for a repeatable one, and yet another for an undo migration in editions that support it. The number that follows is the version itself, and it can contain dots to express minor ordering, so “2”, “2.1”, and “2.2” are all valid and sort in that exact sequence. Two underscores act as a strict separator between the version and the human-readable description, and everything after that separator — with underscores swapped for spaces when Flyway reports on it — becomes the migration’s description in tools, logs, and the history table. A repeatable migration simply omits the version portion entirely, since by definition it has no fixed position in the sequence.
Where migrations live
Migration files do not have to sit in a single folder on disk. Flyway can be configured to look in multiple “locations” at once — a filesystem folder, a location bundled inside a compiled application package, or even a remote location such as cloud object storage — and it treats all of them as one combined pool of available migrations when it resolves what is pending. This flexibility is what allows a single company-wide base set of migrations to be shared across several related services, while each service also contributes its own service-specific migrations from a second location.
Flyway treats the description in a filename as purely cosmetic — it appears in logs and in the history table to help humans understand what a migration does, but Flyway itself never parses or acts on the words in the description. Only the version number and file content matter to its logic.
Under The Hood
BInternal Working
Flyway’s trustworthiness comes down to one deceptively simple table that it creates and manages inside your own database.
The schema history table
The very first time Flyway runs against a database, it creates a table — by default named flyway_schema_history — inside that database. This table is the single source of truth for everything Flyway knows. Every time a migration is successfully applied, Flyway inserts one row recording: the version number, the description, the type of migration, the name of the script, a checksum of its contents, who applied it, when it was applied, how long it took, and whether it succeeded. Nothing about Flyway’s behavior lives outside this table and the migration files themselves — there is no separate hidden database, no external server, and no cloud service required.
graph LR
subgraph Source["Your Project"]
A["Versioned & Repeatable
Migration Files"]
end
subgraph Engine["Flyway Engine"]
B["Migration
Resolver"]
C["Checksum
Validator"]
D["Migration
Executor"]
end
subgraph DB["Target Database"]
E[("flyway_schema_history
table")]
F[("Application
Schema")]
end
A --> B
B --> C
C --> D
D --> F
D --> E
E --> C
Fig. 1 — How Flyway’s engine sits between your migration files and the live database schema.
What a checksum is, and why it matters
A checksum is a short fingerprint calculated from the exact bytes of a file’s content. Change even a single character in a migration script — a typo, an extra space, a renamed column — and the checksum changes completely. Flyway stores the checksum of every migration it applies. On every future run, before applying anything new, Flyway recalculates the checksum of every migration file that has already been marked as applied and compares it against the stored value. If they no longer match, Flyway refuses to continue and raises a validation error.
This checksum check is what prevents “silent drift” — a situation where someone edits an already-applied migration file after the fact, so the file in version control no longer matches what was actually run on production. Without this check, two developers could believe they are running identical migrations while their databases have quietly diverged.
Migration resolution
On every run, Flyway’s resolver scans every configured location (a folder, a classpath, a cloud storage location, and so on) for files that match its naming pattern, parses out the version and description from each filename, and builds an in-memory list of “available migrations.” It then reads the flyway_schema_history table to build a list of “applied migrations.” Comparing these two lists produces the list of “pending migrations” — the ones that exist as files but have no matching row in the history table. This comparison, not the raw contents of the database schema itself, is what Flyway relies on to decide what to do next. Flyway does not inspect your tables and columns to guess what has changed; it only trusts its own history table.
Locking to prevent double-application
If two instances of an application start up at the same moment — for example, during a rolling deployment where five new servers all boot in parallel — they could all try to run the same pending migration simultaneously. To prevent this, Flyway takes a database-level lock before running migrations, so only one process at a time can apply pending changes while the others wait, and then simply see that there is nothing left to do once the lock is released.
Picture a single-lane bridge with a toll gate at each end. If five cars arrive at the exact same moment, the gate only lets one through at a time; the rest simply queue up and wait their turn. None of them try to squeeze through side by side and collide in the middle. Flyway’s lock is that toll gate — it does not stop multiple application instances from starting at once, it just makes sure only one of them is ever actually changing the schema at any given moment.
The validate command
Separately from a full migration run, Flyway offers a validate operation that performs the checksum comparison step on its own, without applying anything new. Teams often run validate as an early, fast check in a build pipeline — before spending time deploying an entire application — specifically to catch the case where someone has edited an already-applied file. Because validate only reads the history table and recalculates checksums, it is a cheap, safe, read-mostly operation that can run as often as needed.
The info command
Flyway also provides an info operation that prints a full table of every migration it knows about — applied and pending — alongside its version, description, type, installed date, and current state. This single view is often the fastest way for a developer or an operator to answer the question “what exactly has and has not been applied to this particular database right now,” without needing to query the history table by hand.
Out-of-order migrations
By default, Flyway insists that migrations apply strictly in ascending version order relative to what has already run. In larger teams working across multiple long-lived branches, however, it is common for a lower-numbered migration to be merged into the main line of development after a higher-numbered one has already been applied elsewhere. Flyway can be configured to allow this “out-of-order” application as a deliberate, explicit choice, but doing so trades away some of the strict guarantees the tool otherwise provides, so most teams reserve it for well-understood, low-risk situations rather than treating it as a default habit.
Step By Step
CData Flow & Lifecycle
Here is the full journey a single migration takes, from the moment a developer writes it to the moment it becomes a permanent part of the database’s history.
graph TD
A["Developer writes a new
migration file, e.g. V2__add_users_table.sql"] --> B["File is committed
to version control"]
B --> C["Flyway command
'migrate' is run"]
C --> D{"Does flyway_schema_history
table exist?"}
D -- No --> E["Create the
history table"]
D -- Yes --> F["Read all applied
migrations from history"]
E --> F
F --> G["Validate checksums of
already-applied migrations"]
G --> H["Compare available files
vs. applied history"]
H --> I{"Any pending
migrations?"}
I -- No --> J["Exit — schema
already up to date"]
I -- Yes --> K["Sort pending migrations
by version number"]
K --> L["Apply each migration
inside a transaction"]
L --> M["Insert success row +
checksum into history"]
M --> N["Schema is now
up to date"]
Fig. 2 — The full lifecycle of a single “flyway migrate” run, from file to applied schema.
Transactions and safety
On databases that support transactional DDL (structural changes wrapped in a rollback-able transaction), Flyway wraps each migration in its own transaction. If anything inside that migration fails partway through, the entire migration is rolled back as though it never ran, and the history table is not updated — the schema is left exactly as it was before the attempt. On databases where the underlying engine does not support transactional DDL for certain statements, a failed migration can leave the schema partially applied, which is why Flyway marks it in the history table as failed and refuses to proceed further until a human resolves the situation, typically by fixing the script and using a repair command.
The repair command
Sometimes a migration genuinely needs to be corrected after the fact — for instance, if it failed halfway and the underlying database doesn’t support rolling back the specific statement that broke. Flyway includes a repair operation that removes failed entries from the history table and can also refresh stored checksums for legitimate, agreed-upon edits to already-applied scripts. This is treated as an exceptional, manual operation rather than part of the everyday flow, precisely because bypassing checksum validation is what schema drift protection exists to prevent in the first place.
Where migration commands fit inside a deployment
In most real deployment pipelines, the migrate step happens at a specific, well-defined point: after new application code has been built and packaged, but before the new version of the application is allowed to start serving live traffic. This ordering matters because it guarantees the database structure the new code expects is already in place the moment that code goes live, rather than the new code starting up against an old schema and immediately failing. In container-based deployments, this is frequently implemented as a dedicated migration step or a short-lived “init container” that runs to completion before the main application container is allowed to start.
Multiple environments, one sequence
Because the schema history table lives inside each individual database, a development database, a staging database, and a production database each keep their own independent history, even though they are usually working through the exact same ordered set of migration files. A migration is typically applied first against a local or development database, then against staging as part of an automated pipeline, and only afterward against production — with the same files, in the same order, every time. This repetition across environments is precisely what gives teams confidence that whatever was tested in staging is what will actually happen in production, rather than production receiving a subtly different set of changes.
Weighing It Up
DAdvantages, Disadvantages & Trade-offs
Flyway is deliberately simple, and that simplicity is both its greatest strength and its main limitation.
Advantages
- Plain SQL files are the primary format, so there is nothing new to learn beyond the SQL your team already writes.
- The history table gives every environment — a laptop, a staging server, production — a single, auditable, agreed-upon truth about what has run.
- Checksum validation catches accidental or unauthorized edits to already-applied scripts before they can cause silent drift.
- Works the same way whether run from a command line, a build tool plugin, a Docker container, or embedded directly inside application startup code.
- Supports a wide range of relational databases through one consistent mental model.
Disadvantages & Trade-offs
- Flyway applies forward changes; true “undo” migrations exist only in certain editions, so most teams roll forward with a corrective migration instead of rolling back.
- Because it only trusts its own history table, if that table is ever manually altered or dropped, Flyway’s understanding of reality becomes unreliable.
- Very large, long-running migrations (for example, rewriting a huge table) still carry the same locking and performance risks any manual schema change would — Flyway coordinates when scripts run, but it does not make an individual heavy statement itself faster or safer on the database engine.
- Multiple developers working on parallel branches can still pick the same version number by accident, which requires a small amount of team discipline or tooling to avoid.
Weighing the trade-off in practice
The clearest way to see the trade-off is to compare life before and after adopting a tool like Flyway. Before, a team typically relies on someone remembering to run a script, emailing a “please run this on production” message, or manually clicking through steps in a database GUI — approaches that work fine with two developers and fail unpredictably once a team grows past that size or once more than one environment exists. After adopting versioned migrations, the same steps become a checked-in file that travels through code review exactly like application code, and the history table becomes an automatic, always-current record of exactly what state every environment is in. The cost of this improvement is discipline: migrations must be treated as permanent history rather than editable drafts, and that discipline has to be adopted by everyone touching the database, not just the person who introduced the tool.
| Approach | Order Guaranteed? | Auditable History? | Detects Tampering? |
|---|---|---|---|
| Manual scripts run by hand | No | No | No |
| Shared “run this SQL” documents | Loosely | Partial, informal | No |
| Flyway versioned migrations | Yes | Yes, in-database | Yes, via checksums |
Doing It Right
EDesign Patterns & Anti-patterns
Over years of use across many teams, a handful of clear patterns — and clear mistakes — have emerged.
Pattern: expand-and-contract
Instead of renaming a column in one risky step (which breaks any code still expecting the old name), teams add the new column alongside the old one, write to both during a transition period, migrate reads over to the new column, and only remove the old column in a later, separate migration once nothing depends on it anymore. This spreads a risky change across several small, reversible-feeling steps.
Pattern: one migration, one intent
Successful teams keep each migration file focused on a single logical change — one new table, one new index, one data backfill — rather than bundling many unrelated changes into one large script. Small migrations are easier to review, easier to reason about if something fails, and easier to search through later.
Pattern: separating structural changes from data changes
Some teams draw a clear line between migrations that change structure (adding a table, adding a column) and migrations that change or move data (backfilling a value, correcting bad rows). Keeping these conceptually distinct — even if both are still ordinary versioned migrations in Flyway’s eyes — makes it easier to reason about risk: a structural change is usually fast and low-risk on an empty column, while a data change touching every existing row is where real performance and correctness risk tends to live.
Pattern
Editing an already-applied migration file directly, instead of writing a new migration to correct the mistake.
Why It Happens
It feels faster to “just fix the typo in the old file” than to write a whole new script for a small correction.
Consequence
The checksum stored in the history table no longer matches the file, so Flyway raises a validation failure on every environment that already applied the original version — usually discovered at the worst possible time, during a deployment.
Better Approach
Always write a new, forward-only migration that corrects the mistake, and treat every already-applied file as permanently frozen.
Pattern: environment-specific configuration, not environment-specific scripts
Rather than maintaining separate migration folders for development, staging, and production, mature teams keep one shared set of migration files and vary only Flyway’s connection configuration per environment. This guarantees that the exact same sequence of changes reaches production that was already validated in staging.
Pattern: backfill in small batches
When a migration needs to populate a new column for millions of existing rows, doing it in one giant statement can lock a table for an uncomfortably long time. A common pattern is to add the new column as nullable in one migration, then perform the actual backfill in small, chunked batches — either through a follow-up migration written carefully with batching logic, or through a separate background process outside of Flyway entirely — and only make the column mandatory once every row has a value. This keeps any single migration fast and keeps locks short.
Pattern
Renaming a column directly in a single migration while application code that still expects the old name is running in production.
Why It Happens
A rename looks like the “obvious” fix when a column was poorly named, and it seems simpler than adding a new column.
Consequence
Any application instance still running the previous version of the code — which is normal during a rolling deployment — immediately starts failing every query that references the old column name.
Better Approach
Use the expand-and-contract pattern: add the new column, migrate application code over a deployment cycle, then remove the old column only once nothing references it.
Staying Out Of Trouble
FBest Practices & Common Mistakes
Most Flyway incidents are avoidable and fall into a small, repeatable set of categories.
Never Reuse A Version Number
If two people accidentally create the same version number on different branches, whichever merges second will fail validation or, worse, silently apply out of the intended order. Reserve version numbers as soon as a migration is started, or use a timestamp-based numbering scheme to avoid collisions entirely.
Treat Applied Migrations As Immutable
Once a migration has run anywhere outside your own machine, it should never be edited again. If it was wrong, correct it forward with a brand-new migration.
Keep Migrations Idempotent-Safe Where Possible
Writing statements that check for existence before creating something (where the database supports it) reduces the blast radius if a migration needs to be re-examined or re-run in a recovery scenario.
Run Migrations As Part Of Deployment, Not As A Manual Afterthought
Teams that wire “flyway migrate” into their automated deployment pipeline avoid the classic mistake of forgetting to apply a migration on one environment while remembering it on another.
Back Up Before Large Structural Changes
Flyway coordinates when a change runs; it is not a substitute for a database backup or snapshot before a large or destructive structural change.
Test every migration against a copy of production-scale data before it reaches production. A migration that runs instantly on an empty development table can lock a table with tens of millions of rows for minutes in production.
Common mistake: assuming migrations are optional in a hotfix
Under deadline pressure, it is tempting to make a quick change directly on the production database “just this once” and add a matching migration file later. This immediately breaks the guarantee that the history table reflects reality, because the change now exists on production without ever having gone through the checksum-tracked process, and the next environment to run migrations from scratch will apply a script for a change that already silently exists elsewhere with no record. Treating every structural change, including urgent ones, as a migration file first keeps the history trustworthy even under pressure.
Common mistake: ignoring migration duration in CI
Teams sometimes only notice that a migration is slow when it runs against production for the first time, because their automated test databases are small and empty. Measuring how long each migration takes against a realistically sized dataset, and treating a sudden jump in duration as a signal worth investigating before merging, catches most performance surprises early rather than during a live deployment window.
Common mistake: skipping code review for migration files
Because migration files are just SQL, some teams treat them as less important than application code and merge them without the same review rigor. In practice, a mistaken migration can cause outages or data loss that application code bugs rarely can, which is a strong argument for holding migration files to at least the same review standard as any other change to a shared, production system.
Seeing It In Practice
GReal-World & Industry Examples
Schema migration tooling like Flyway shows up anywhere a relational database backs a fast-moving application with more than one contributor.
Fast-Growing SaaS Startups
A small team shipping a new feature every week needs its database structure to evolve at the same pace as its code, across development laptops, a staging environment, and production, all without a dedicated database administrator manually applying each change by hand.
E-Commerce Platforms
Order, inventory, and payment tables change constantly as new features (discount codes, loyalty points, regional tax rules) are added. A single missed or misordered structural change on a high-traffic checkout table can cause real financial impact, so an auditable, ordered history of every change becomes essential.
Financial Services & Fintech
Regulatory audits often require a complete, provable history of every structural change made to systems holding financial records. A tool that automatically records who ran what, when, and with what checksum provides exactly the kind of trail an audit asks for.
Enterprise Microservice Fleets
Large organizations running dozens or hundreds of independent services, each with its own database, use migration tooling embedded directly into each service’s startup process, so a service and its own private schema always move forward together as a single deployable unit.
Healthcare And Insurance Systems
Systems that store patient or policyholder records tend to have unusually long-lived schemas, sometimes maintained for a decade or more across many teams that rotate over time. A permanent, in-database record of every structural change becomes the only reliable way for a new engineer, years later, to understand exactly how the current schema came to look the way it does.
Continuous Integration And Testing Pipelines
Automated test suites frequently need a freshly built database that exactly matches production structure. Running the full set of migrations from an empty database as part of a build pipeline is a common way to both create that fresh test database and continuously verify that every migration in the repository still runs cleanly from scratch.
Why the pattern spread so widely
Schema migration tooling did not become common because any single company invented a clever trick — it became common because the underlying problem is universal. Any application with a relational database, more than one contributor, and more than one environment eventually runs into the same coordination failure that opened this article: people changing structure independently, with no shared record. Once a team has been burned by that failure even once, adopting an ordered, checksum-verified history stops being an optional nicety and starts being treated as basic infrastructure, in the same category as version control itself.
Common Questions
HFAQ
Wrapping Up
ISummary & Key Takeaways
Flyway turns database schema changes from an unrecorded, error-prone habit into an ordered, auditable, repeatable process.
At its heart, Flyway solves a coordination problem: many people and many environments need to agree, at all times, on exactly which structural changes a database has already gone through. It does this with one honest, simple mechanism — a history table inside the database itself, backed by checksums that detect any unauthorized tampering with already-applied scripts. Versioned migrations move a schema forward in a fixed order, repeatable migrations keep view-like objects always current, and a small set of safety commands (validate, repair, baseline, info) handle the edge cases that come up once real teams and real production incidents enter the picture.
None of this removes the need for good engineering judgment. Large or destructive changes still deserve a backup beforehand, a test against realistic data, and careful review, exactly as they would without any tooling at all. What changes is that every environment — a laptop, a shared staging server, and production — now moves through exactly the same, ordered, provable sequence of changes, and any attempt to quietly diverge from that sequence gets caught by a checksum before it can cause confusion weeks or months later. That single property — a trustworthy, shared memory of “what happened to this schema, in what order, and who is responsible for it” — is the entire reason schema migration tooling exists, and it is what makes it possible for a team of any size to change a live database with confidence instead of guesswork.
Key Takeaways
- A schema is the structural skeleton of a database — tables, columns, types, relationships — and migration is the controlled process of changing that skeleton safely over time.
- Flyway’s entire memory lives in one table,
flyway_schema_history, inside your own database — there is no external service holding the truth. - Versioned migrations run exactly once, in strict numerical order; repeatable migrations re-run whenever their content changes, with no fixed order.
- Checksums prevent silent drift by detecting any edit made to a migration file after it has already been applied somewhere.
- Never edit an applied migration — always correct mistakes forward with a brand-new migration file.
- Flyway coordinates when changes run; it does not replace backups, staging tests, or careful review of large structural changes.
- The same migration files should reach every environment — development, staging, and production — so what was tested is exactly what ships.