Введение: зачем монорепозиторий в Kotlin?
В современной разработке на Kotlin часто возникает дилемма: хранить несколько связанных проектов (бэкенд, микросервисы, библиотеки, Android-приложения) в отдельных репозиториях (полирепозиторий) или объединить их в один монорепозиторий. Монорепозиторий — это стратегия хранения кода, при которой все компоненты находятся в единой системе контроля версий. Для Kotlin-проектов это особенно актуально благодаря мощной системе сборки Gradle и встроенной поддержке многомодульности.
В этой статье мы разберём практические аспекты организации монорепозитория с Kotlin: от структуры директорий до настройки CI/CD и управления зависимостями. Вы узнаете, как избежать типичных ошибок и получить максимальную выгоду от единого хранилища кода.
1. Структура монорепозитория: от теории к практике
Правильная структура — основа успешного монорепозитория. Рассмотрим типовой проект, включающий REST API на Ktor, библиотеку для работы с базой данных и Android-приложение. Вот рекомендуемая иерархия:
my-monorepo/
├── build.gradle.kts # Корневой файл сборки
├── settings.gradle.kts # Настройка модулей
├── gradle.properties # Общие свойства
├── gradle/
│ └── libs.versions.toml # Каталог версий (Version Catalog)
├── libs/ # Общие библиотеки
│ ├── common-utils/ # Утилиты, расширения
│ ├── common-db/ # Работа с БД (Exposed)
│ └── common-dto/ # Data Transfer Objects
├── services/ # Серверные приложения
│ ├── user-service/ # Микросервис пользователей
│ └── order-service/ # Микросервис заказов
└── apps/ # Клиентские приложения
└── android-app/ # Android-приложение
Ключевой элемент — settings.gradle.kts. Он включает все модули:
rootProject.name = "my-monorepo"
include("libs:common-utils")
include("libs:common-db")
include("libs:common-dto")
include("services:user-service")
include("services:order-service")
include("apps:android-app")
Такая структура позволяет легко переиспользовать код между сервисами и клиентом, а также централизованно управлять версиями зависимостей.
1.1. Version Catalog: управление зависимостями
В монорепозитории критически важно избегать дублирования версий библиотек. Используйте Version Catalog (файл gradle/libs.versions.toml):
[versions]
kotlin = "2.0.21"
ktor = "3.0.3"
exposed = "0.57.0"
[libraries]
ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }
exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
Теперь в любом модуле вы можете писать:
dependencies {
implementation(libs.ktor.server.core)
implementation(libs.exposed.core)
}
Это гарантирует, что все модули используют одинаковые версии, и упрощает массовое обновление.
2. Практическая организация модулей: Gradle Multi-Project
Каждый модуль монорепозитория — это отдельный подпроект Gradle со своим build.gradle.kts. Рассмотрим настройку библиотеки common-utils:
// libs/common-utils/build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
}
group = "com.mycompany.common"
version = "1.0.0"
dependencies {
implementation(kotlin("stdlib"))
implementation(libs.kotlinx.coroutines)
}
А вот типовой микросервис на Ktor:
// services/user-service/build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
alias(libs.plugins.ktor)
}
dependencies {
implementation(project(":libs:common-utils"))
implementation(project(":libs:common-dto"))
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
}
Обратите внимание на project(":libs:common-utils") — это ссылка на другой модуль монорепозитория. Gradle автоматически соберёт его первым и подключит как локальную зависимость.
2.1. Общие правила для модулей
- Избегайте циклических зависимостей: если модуль A зависит от B, а B от A — Gradle выдаст ошибку. Проектируйте иерархию сверху вниз.
- Используйте API и implementation: для зависимостей, которые должны быть видны потребителям модуля, используйте
api(), для внутренних —implementation(). - Выносите общий код в libs/: DTO, утилиты, интерфейсы репозиториев — всё, что используется в двух и более модулях, должно жить в общих библиотеках.
3. CI/CD для монорепозитория: умные пайплайны
Главная боль монорепозитория — время сборки. Нет смысла собирать Android-приложение, если изменился только бэкенд. Решение — инкрементальные пайплайны, которые запускают сборку только для изменённых модулей и их зависимостей.
Пример для GitHub Actions с использованием paths и paths-ignore:
name: Build Changed Modules
on:
push:
branches: [main]
paths:
- 'services/**'
- 'libs/**'
- 'gradle/**'
- 'build.gradle.kts'
- 'settings.gradle.kts'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Build affected modules
run: |
./gradlew :services:user-service:build :services:order-service:build
Более продвинутый подход — использовать Gradle Build Cache и Composite Builds. Они позволяют кешировать результаты сборки неизменённых модулей