Database Migrations in Kotlin: Overview of Tools and Approaches

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: A Complete Guide to Tools and Approaches



Managing database schema changes is one of the key tasks when developing any application. In the Kotlin ecosystem, there are several mature solutions that allow you to automate this process. In this article, we will cover three main approaches: built-in ORM Exposed tools, the classic Flyway, and the powerful Liquibase. You will learn how to integrate them into a Kotlin project and which tool to choose for specific tasks.



1. Why are migrations needed?

Manually executing SQL scripts in different environments (local development, staging, production) inevitably leads to schema desynchronization. Migrations solve this problem by providing:

  • Versioning of database changes
  • Automatic application of new scripts when the application starts
  • The ability to rollback to a previous version
  • Team collaboration without schema conflicts


2. Exposed: Native Migration Support

Exposed is a lightweight ORM from JetBrains written in Kotlin. Starting from version 0.41.0, the library provides its own migration mechanism that fits perfectly into the Kotlin ecosystem.



Installation

// build.gradle.ktsimplementation("org.jetbrains.exposed:exposed-core:0.55.0")implementation("org.jetbrains.exposed:exposed-dao:0.55.0")implementation("org.jetbrains.exposed:exposed-migrations:0.55.0")


Creating a Migration

Each migration is a class that inherits from VoidDatabaseMigration:

import org.jetbrains.exposed.sql.transactions.transactionimport org.jetbrains.exposed.sql.SchemaUtilsimport org.jetbrains.exposed.sql.Tableimport org.jetbrains.exposed.migrations.VoidDatabaseMigration

object Users : Table() { val id = integer("id").autoIncrement() val name = varchar("name", 255) override val primaryKey = PrimaryKey(id)}

class V1__CreateUsersTable : VoidDatabaseMigration() { override fun migrate() { transaction { SchemaUtils.create(Users) } }}

class V2__AddEmailColumn : VoidDatabaseMigration() { override fun migrate() { transaction { SchemaUtils.addColumn(Users, Users.varchar("email", 255)) } }}


Running Migrations

import org.jetbrains.exposed.migrations.ExposedMigrationUtilsimport javax.sql.DataSource

fun runMigrations(dataSource: DataSource) { ExposedMigrationUtils.runMigrations( dataSource = dataSource, migrationsPackage = "com.example.migrations" )}


Pros: fully Kotlin-style, no SQL files, typed column names.
Cons: tied to Exposed, no visual tracker, no rollback out of the box.



3. Flyway: Simplicity and Reliability

Flyway is the most popular migration tool in the JVM world. It works with any database and is independent of ORM. In Kotlin, it is used via the official SDK or Gradle plugin.



Setup

// build.gradle.ktsimplementation("org.flywaydb:flyway-core:10.20.0")// For PostgreSQL, add the driver:implementation("org.postgresql:postgresql:42.7.4")


Migration Structure

Flyway uses SQL files with specific naming:

db/migration/   V1__create_users.sql   V2__add_email_column.sql   V3__add_role_column.sql


Example SQL Migration (V1__create_users.sql)

CREATE TABLE users (    id SERIAL PRIMARY KEY,    name VARCHAR(255) NOT NULL,    email VARCHAR(255) UNIQUE);


Integration with Kotlin

import org.flywaydb.core.Flywayimport javax.sql.DataSou                    

Recommendations