Монорепозиторий с Kotlin: организация кода и лучшие практики
С ростом проектов и увеличением числа сервисов перед разработчиками встаёт вопрос: как организовать код так, чтобы он оставался управляемым, легко тестируемым и быстро разворачиваемым? Один из популярных подходов — монорепозиторий (monorepo). В этой статье мы разберём, как эффективно организовать монорепозиторий на Kotlin с использованием Gradle, какие инструменты и паттерны применять, и как избежать типичных ошибок.
Что такое монорепозиторий и зачем он нужен?
Монорепозиторий — это подход, при котором код нескольких проектов (модулей, библиотек, сервисов) хранится в одном репозитории. В контексте Kotlin это означает, что вы можете иметь общий Gradle-проект, внутри которого находятся модули для backend-сервисов, Android-приложений, общих библиотек и даже документации.
Преимущества монорепозитория:
- Единая версионизация — все модули используют одну версию Kotlin и общих зависимостей.
- Упрощённый рефакторинг — изменения в общей библиотеке сразу видны всем потребителям.
- Общая настройка CI/CD — один пайплайн для всех модулей.
- Улучшенная коллаборация — разработчики видят весь код проекта.
Однако монорепозиторий требует дисциплины: без правильной организации он может превратиться в «монолит» с запутанными зависимостями. Kotlin и Gradle предоставляют отличные инструменты для борьбы с этим.
Структура монорепозитория на Kotlin
Рассмотрим типовую структуру проекта, где есть backend-сервис на Ktor, общая библиотека для моделей данных и Android-приложение:
my-monorepo/
├── build.gradle.kts # корневой файл сборки
├── settings.gradle.kts # подключение модулей
├── gradle.properties # общие версии
├── common/
│ ├── build.gradle.kts
│ └── src/
│ └── main/kotlin/
│ └── com/example/common/
│ └── Models.kt
├── backend/
│ ├── build.gradle.kts
│ └── src/
│ └── main/kotlin/
│ └── com/example/backend/
│ └── Application.kt
├── android-app/
│ ├── build.gradle.kts
│ └── src/
│ └── main/kotlin/
│ └── com/example/android/
│ └── MainActivity.kt
└── ci/
└── build.sh
Файл settings.gradle.kts подключает модули:
rootProject.name = "my-monorepo"
include(":common")
include(":backend")
include(":android-app")
Корневой build.gradle.kts обычно содержит только общие плагины и настройки:
plugins {
kotlin("jvm") version "1.9.22" apply false
kotlin("android") version "1.9.22" apply false
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.22" apply false
}
allprojects {
group = "com.example"
version = "1.0.0"
}
Важно: плагины указываются с apply false, чтобы они не применялись к корневому проекту, а были доступны дочерним модулям.
Управление зависимостями в монорепозитории
Одна из главных задач — избежать дублирования версий зависимостей. Для этого используют Gradle Version Catalog (файл libs.versions.toml в папке gradle). Пример:
# gradle/libs.versions.toml
[versions]
kotlin = "1.9.22"
ktor = "2.3.7"
coroutines = "1.7.3"
[libraries]
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "ktor" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Теперь в модулях можно ссылаться на каталог:
// backend/build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
}
dependencies {
implementation(project(":common"))
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.kotlinx.coroutines)
}
Такой подход гарантирует, что все модули используют одинаковые версии библиотек, а при обновлении достаточно изменить один файл.
Организация модулей: общие библиотеки и изоляция
В монорепозитории важно чётко разделять ответственность модулей. Рекомендуется выделить:
- Common / Shared — модуль с моделями данных, DTO, общими утилитами. Он не должен зависеть от фреймворков.
- Domain — бизнес-логика, которая не зависит от инфраструктуры.
- Infrastructure — работа с БД, внешними API, кэшированием.
- Application / Service — точки входа (Ktor, Spring Boot, Android Activity).
Пример модуля common:
// common/build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.kotlin.serialization)
}
dependencies {
implementation(libs.kotlinx.serialization.json)
// никаких зависимостей от Ktor или Android!
}
Код внутри common:
// common/src/main/kotlin/com/example/common/Models.kt
package com.example.common
import kotlinx.serialization.Serializable
@Serializable
data class User(
val id: Long,
val name: String,
val email: String
)
Backend-сервис использует этот модуль через implementation(