Database Migrations in Kotlin: Tools and Practical Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Database Migrations in Kotlin: How to Manage Schema Without Pain

When developing in Kotlin, especially in conjunction with JVM, sooner or later the task of synchronizing the database structure between developers, test, and production servers arises. Manually executing SQL scripts leads to desynchronization and errors. The solution is database migrations. This is an automated way to version schema changes: creating tables, adding columns, indexes, and triggers.

In this article, we'll look at three popular tools for Kotlin: Flyway, Liquibase, and the built-in mechanism of Exposed. You'll learn how to integrate them into a project, write migrations, and avoid common problems.



Flyway: Simplicity and Versioning

Flyway is the most popular migration tool in the JVM ecosystem. It supports Kotlin, Java, and Groovy. The principle of operation: you write SQL scripts (or Java/Kotlin classes), and Flyway tracks which ones have already been applied using the flyway_schema_history table.

Adding to Gradle (Kotlin DSL):

dependencies {    implementation("org.flywaydb:flyway-core:9.22.3")    // for PostgreSQL    implementation("org.flywaydb:flyway-database-postgresql:9.22.3")}

Example migration V1__create_users.sql:

CREATE TABLE users (    id SERIAL PRIMARY KEY,    username VARCHAR(50) NOT NULL UNIQUE,    email VARCHAR(100) NOT NULL,    created_at TIMESTAMP DEFAULT NOW());

CREATE INDEX idx_users_email ON users(email);

Running from Kotlin code:

import org.flywaydb.core.Flyway

fun main() { val flyway = Flyway.configure() .dataSource("jdbc:postgresql://localhost:5432/mydb", "user", "pass") .load() flyway.migrate()}

Advantages:

  • Minimal configuration
  • Support for rollbacks (undo) in the paid version
  • Works with any SQL

Disadvantages:

  • No built-in schema validation
  • Difficult to manage branching


Liquibase: Flexibility and Declarative Approach

Liquibase offers two approaches: changes can be described in XML, YAML, JSON, or directly in SQL. For Kotlin projects, the YAML format is convenient — it's readable and easily versioned.

Adding to Gradle:

dependencies {    implementation("org.liquibase:liquibase-core:4.24.0")}

Example changelog file db.changelog-master.yaml:

databaseChangeLog:  - changeSet:      id: 1      author: developer      changes:        - createTable:            tableName: users            columns:              - column:                  name: id                  type: BIGINT                  autoIncrement: true                  constraints:                    primaryKey: true              - column:                  name: username                  type: VARCHAR(50)                  constraints:                    nullable: false                    unique: true              - column:                  name: email                  type: VARCHAR(100)        - createIndex:            indexName: idx_users_email            tableName: users            columns:              - column:                  name: email

Running from Kotlin:

import liquibase.Liquibaseimport liquibase.database.jvm.JdbcConnectionimport liquibase.resource.ClassLoaderResourceAccessorimport java.sql.DriverManager

fun runMigrations() { val connection = DriverManager.getConnection( "jdbc:postgresql://localhost:5432/mydb", "user", "pass" ) val database = DatabaseFactory.getInstance() .findCorrectDatabaseImplementation(JdbcConnection(connection)) val liquibase = Liquibase("db.changelog-mas

Recommendations