Проект на Ktor вырос из MVP в большой production-сервис. Какие проблемы архитектуры и сопровождения вы ожидаете увидеть?
MVP на Ktor вырастает в хаос из-за God-module, отсутствия слоёв, ручного DI, нетипизированных конфигов и пропущенной observability. Решение — явная слоистая архитектура, Koin/Hilt, typed config и трассировка с первого дня.
Рост Ktor-проекта: архитектурные проблемы
MVP на Ktor обычно начинается с одного файла Application.kt, где вперемешку лежат роуты, бизнес-логика и обращения к базе. При росте это превращается в монолит, который сложно тестировать и масштабировать.
God-module и отсутствие слоёв
Проблема: один fun Application.module() знает о БД, внешних HTTP-клиентах, кэше и бизнес-правилах одновременно. Решение — явные слои и разделение ответственности:
// Application.kt — только сборка
fun Application.module() {
configureDI() // Koin
configureSecurity()
configureRouting()
configureMonitoring()
}
// routing/UserRoutes.kt
fun Route.userRoutes(userService: UserService) {
get("/users/{id}") {
val id = call.parameters["id"]!!
call.respond(userService.getById(id))
}
}
Ручной DI
В MVP зависимости часто создаются вручную внутри роутов. При росте это приводит к сложным конструкторам и невозможности мока в тестах. Используйте Koin:
val appModule = module {
single { HikariDataSource(hikariConfig()) }
single<UserRepository> { PostgresUserRepository(get()) }
single { UserService(get()) }
}
fun Application.configureDI() {
install(Koin) {
modules(appModule)
}
}
Нетипизированные конфиги
Обращения к environment.config.property("ktor.database.url").getString() по всему коду — хрупко и нетестируемо. Вынесите в data class с валидацией при старте:
data class AppConfig(
val databaseUrl: String,
val jwtSecret: String,
val redisHost: String
)
fun ApplicationConfig.toAppConfig() = AppConfig(
databaseUrl = property("database.url").getString(),
jwtSecret = property("jwt.secret").getString(),
redisHost = property("redis.host").getString()
)
Отсутствие observability
В MVP нет структурных логов и метрик. При incident в production невозможно понять, что произошло. Добавьте с первого дня:
install(MicrometerMetrics) {
registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
distributionStatisticConfig = DistributionStatisticConfig.Builder()
.percentilesHistogram(true)
.build()
}
install(CallLogging) {
level = Level.INFO
format { call ->
val status = call.response.status()
val uri = call.request.uri
val method = call.request.httpMethod.value
val duration = call.processingTimeMillis()
"$method $uri -> $status in ${duration}ms"
}
mdc("requestId") { call ->
call.request.header("X-Request-Id") ?: UUID.randomUUID().toString()
}
}
Проблемы сопровождения
- Версионирование API: без
/v1/префикса обратная совместимость ломается с первым breaking change. Используйтеroute("/v1") { ... }с самого начала. - Error handling: без централизованного
StatusPagesразные роуты возвращают разные форматы ошибок. Установите один обработчик для всехException. - Миграции БД: без Flyway/Liquibase схема расходится между средами. Добавьте Flyway в стартап-последовательность.
Подводные камни
- Расширение одного
module()вместо разбиения на feature-модули — файл растёт до тысяч строк. - Shared mutable state в companion object или global val без синхронизации — race condition под нагрузкой.
- Отсутствие circuit breaker для внешних HTTP-вызовов — один зависший downstream кладёт всё приложение.
- Тесты только на unit-уровне без интеграционных — поведение плагинов не покрыто.
- Логи в
println()илиSystem.out— теряются в контейнерных средах без структуры. - Отсутствие health-check эндпоинта — Kubernetes не знает о состоянии приложения.
- Hardcoded timeout'ы или их полное отсутствие в HttpClient — cascading failures.
- Нет rate limiting — открытые эндпоинты эксплуатируются при росте трафика.
What hurts your answer
- Говорить только о запуске Ktor, но не об эксплуатации
- Не упоминать observability, обновления, безопасность и rollback
- Описывать риски абстрактно, без способов их снижать
What they're listening for
- Видит production-риски Ktor
- Говорит про monitoring, rollout, rollback и безопасность
- Умеет ранжировать риски по вероятности и влиянию