Ktor + Exposed: Coordinating Schema Migrations Across Stateless Instances
0 reputation · 15 May 2025, 18:12 UTC
0 reputation · 15 May 2025, 18:12 UTC
Ktor handles HTTP routing and coroutine-based concurrency while Exposed provides the SQL DSL and DAO layer for persistence. Schema evolution in Exposed relies on SchemaUtils.createMissingTablesAndColumns, which performs additive DDL only and lacks a built-in version history or rollback mechanism.
When multiple stateless Ktor instances start simultaneously, each may invoke SchemaUtils against the same database. Because the operation is not idempotent under concurrent execution and most backing databases (MySQL, MariaDB) commit DDL implicitly, a partial schema update on one instance cannot be rolled back by the transaction boundary that protects DML work. External tools such as Flyway or Liquibase supply versioned migrations, yet integrating them introduces a second source of truth for the schema.
SchemaUtils so that exactly one instance applies additive changes while others wait safely?34775 reputation · 15 May 2025, 22:54 UTC
Because SchemaUtils.createMissingTablesAndColumns is not designed for distributed coordination and lacks a versioning mechanism, you must implement an external synchronization primitive to prevent concurrent DDL execution. The most reliable pattern for stateless instances is a distributed lock.
migration_lock table. The first instance to successfully update a locked_at timestamp or insert a row gains the right to run SchemaUtils. Other instances should poll this lock or wait until the lock is released before proceeding to start the Ktor server.Establishing a Flyway baseline from an existing Exposed schema cannot be done automatically within the Exposed DSL, as Exposed does not export its internal state to a versioned format. To transition without manual SQL extraction:
mysqldump --no-data) to capture the current state of the schema.V1__Initial_Schema.sql migration file.flyway baseline on the production database to mark all existing tables as part of version 1, preventing Flyway from attempting to recreate them.To prevent blocking the Ktor event loop and ensure data integrity, migrations must be sequenced as a blocking prerequisite to the server startup.
Place the migration logic in the main function or a dedicated Application.module initialization block before the embeddedServer or EngineMain starts listening for requests. Since JDBC is synchronous, wrap the migration call in Dispatchers.IO.
// Example sequencing logic
fun Application.module() {
launch(Dispatchers.IO) {
transaction {
// 1. Acquire Distributed Lock
// 2. SchemaUtils.createMissingTablesAndColumns(...)
// 3. Release Distributed Lock
}
}.join() // Block module loading until migration completes
// Initialize routes and other services
configureRouting()
}
SchemaUtils fails halfway, the database is left in a partial state that cannot be rolled back by the transaction.Diagnostic Detail Needed: Are you utilizing a container orchestrator (like Kubernetes) that supports initContainers? If so, the recommended pattern shifts from application-level locking to moving the migration logic into a separate Init Container, removing the concurrency problem entirely from the Ktor instances.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.