Migration
Database migrations apply schema and reference data changes in a controlled order: they create tables, indexes, constraints, and perform other SQL operations required by a new application version.
In Kora, migration modules are bound to JdbcDatabase initialization through a GraphInterceptor<JdbcDatabase>: when the application starts, JdbcDatabase is created as a graph component, and the interceptor's init() runs migrations before the component is published to the rest of the graph.
If a migration fails, init() throws, so JdbcDatabase component initialization and the whole graph build (application startup) fail as well.
The interceptor's release() is a no-op: migrations are never rolled back or re-run when the application stops.
This approach is convenient for local development, tests, and small installations where the application runs as a single instance. For environments with multiple replicas, choose a separate migration execution method in advance so migrations are not run simultaneously from every application instance. Repositories do not create the database schema themselves: tables, indexes, constraints, and reference data must be created by migrations or by an external database preparation process.
Flyway¶
Module for database migration using the Flyway tool.
During JdbcDatabase initialization, the module calls Flyway.migrate() with settings from the flyway section.
Migrations are run by FlywayJdbcDatabaseInterceptor, which is provided by FlywayJdbcDatabaseModule.
Flyway is wired to SLF4J (loggers("slf4j")), so migration output and the FlyWay migration applied in ... timing line (logged at INFO) appear in the application's normal logs.
Dependency¶
Dependency build.gradle:
Module:
Dependency build.gradle.kts:
Module:
Requires the JDBC module because migrations are executed through DataSource.
Applications usually include both modules: JdbcDatabaseModule creates JdbcDatabase, and FlywayJdbcDatabaseModule adds the migration interceptor.
Configuration¶
Example of the complete configuration described by the FlywayConfig class:
flyway {
enabled = true //(1)!
locations = ["db/migration"] //(2)!
executeInTransaction = true //(3)!
validateOnMigrate = true //(4)!
mixed = false //(5)!
configurationProperties {} //(6)!
}
- Enables migration execution during
JdbcDatabaseinitialization (default:true). If set tofalse, the module skips theFlyway.migrate()call. - Paths to directories with migration scripts (default:
["db/migration"]). - Executes migrations inside a transaction when supported by the database and the
SQLoperations themselves (default:true). - Validates checksums of already applied migrations before executing new ones (default:
true). If checksums do not match, startup fails with an error. - Allows mixing transactional and non-transactional
SQLoperations in one migration (default:false). If enabled, the whole migration is executed without a transaction to avoid errors in databases where some operations cannot run inside a transaction. This setting is relevant for databases that do not support executing certain operations inside a transaction: PostgreSQL, Aurora PostgreSQL, SQL Server, and SQLite. - Additional
Flywaykey-value properties (default:{}). Use them to pass settings that do not have a separate Kora configuration option, such asschemas,baselineOnMigrate,placeholderReplacement, orplaceholders.*.
flyway:
enabled: true #(1)!
locations: ["db/migration"] #(2)!
executeInTransaction: true #(3)!
validateOnMigrate: true #(4)!
mixed: false #(5)!
configurationProperties: {} #(6)!
- Enables migration execution during
JdbcDatabaseinitialization (default:true). If set tofalse, the module skips theFlyway.migrate()call. - Paths to directories with migration scripts (default:
["db/migration"]). - Executes migrations inside a transaction when supported by the database and the
SQLoperations themselves (default:true). - Validates checksums of already applied migrations before executing new ones (default:
true). If checksums do not match, startup fails with an error. - Allows mixing transactional and non-transactional
SQLoperations in one migration (default:false). If enabled, the whole migration is executed without a transaction to avoid errors in databases where some operations cannot run inside a transaction. This setting is relevant for databases that do not support executing certain operations inside a transaction: PostgreSQL, Aurora PostgreSQL, SQL Server, and SQLite. - Additional
Flywaykey-value properties (default:{}). Use them to pass settings that do not have a separate Kora configuration option, such asschemas,baselineOnMigrate,placeholderReplacement, orplaceholders.*.
Migration Files¶
By default, Flyway looks for migrations in src/main/resources/db/migration.
A regular migration file has a name like V1__init_schema.sql, where V1 is the version and the part after the double underscore is the description.
Example of a simple migration:
When Flyway starts, it creates a service migration history table and applies only new versions.
If validateOnMigrate is enabled, already applied files must not be changed without a separate migration history repair process.
Liquibase¶
Module for database migration using the Liquibase tool.
During JdbcDatabase initialization, the module obtains a connection from DataSource, creates a Liquibase instance, and calls update().
Migrations are run by LiquibaseJdbcDatabaseInterceptor, which is provided by LiquibaseJdbcDatabaseModule.
Dependency¶
Dependency build.gradle:
Module:
Dependency build.gradle.kts:
Module:
Requires the JDBC module because migrations are executed through DataSource.
Applications usually include both modules: JdbcDatabaseModule creates JdbcDatabase, and LiquibaseJdbcDatabaseModule adds the migration interceptor.
Configuration¶
Example of the complete configuration described by the LiquibaseConfig class:
Unlike Flyway, the Liquibase module does not have an enabled setting: if the module is connected to the application graph, migrations run during JdbcDatabase initialization.
If a Liquibase migration fails, the module wraps the error in IllegalStateException, and application startup is interrupted.
Migration Files¶
By default, Liquibase looks for the main changelog file at src/main/resources/db/changelog/db.changelog-master.xml.
Liquibase supports different changelog formats, but an SQL-oriented project often benefits from keeping migrations as formatted SQL.
The main file can include such migrations with include.
Minimal main changelog:
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<include file="db/changelog/changes/001-init-users.sql"/>
</databaseChangeLog>
Example of an included migration in formatted SQL:
--liquibase formatted sql
--changeset app:001-init-users
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL
);
Recommendations¶
Recommendation
Migration modules are not recommended for running migrations on application startup in horizontally scaled environments where the application runs with multiple replicas. Each replica will try to execute migrations during startup. Also keep in mind that every application restart triggers the migration mechanism again.
In such cases, use the Flyway Gradle Plugin for local development,
run Flyway from code after database startup in tests,
use a Kubernetes Job for production Kubernetes environments,
or run migrations separately from CI.