Монорепозиторий с Kotlin: организация кода на практике
В современной разработке программного обеспечения всё чаще возникает выбор между монорепозиторием (monorepo) и мультирепозиторием (multirepo). Для проектов на Kotlin, особенно тех, что используют Kotlin Multiplatform или микросервисную архитектуру, монорепозиторий становится мощным инструментом. В этой статье мы разберём, как правильно организовать код в монорепозитории на Kotlin, рассмотрим практические примеры и лучшие практики, которые помогут вам избежать типичных ошибок.
Что такое монорепозиторий и зачем он нужен в Kotlin?
Монорепозиторий — это подход, при котором весь код проекта (или нескольких связанных проектов) хранится в одном репозитории. В контексте Kotlin это может включать:
- Общие библиотеки — утилиты, модели данных, DTO.
- Модули приложений — backend (Ktor, Spring), frontend (Kotlin/JS), мобильные приложения (Kotlin Multiplatform).
- Скрипты и конфигурации — CI/CD, Dockerfile, Gradle-скрипты.
Основные преимущества монорепозитория для Kotlin-проектов:
- Единая версия зависимостей — все модули используют одну версию Kotlin и библиотек, что снижает риск конфликтов.
- Упрощённый рефакторинг — изменение API в одном модуле сразу видно во всех зависимых.
- Легче настраивать CI/CD — один пайплайн для всех компонентов.
- Прозрачность — вся кодовая база доступна в одном месте.
Однако есть и недостатки: размер репозитория может расти, а время клонирования увеличивается. Но для средних и крупных проектов на Kotlin монорепозиторий часто оказывается оптимальным решением.
Структура монорепозитория: практический шаблон
Правильная организация папок — основа успеха. Рассмотрим типовую структуру для Kotlin-проекта с использованием Gradle:
my-kotlin-monorepo/
├── build.gradle.kts # Корневой скрипт сборки
├── settings.gradle.kts # Настройка модулей
├── gradle.properties # Общие параметры Gradle
├── gradle/
│ └── libs.versions.toml # Version Catalog (рекомендуется)
├── modules/
│ ├── common/ # Общие утилиты и модели
│ │ ├── build.gradle.kts
│ │ └── src/
│ ├── backend/ # Backend-сервис на Ktor
│ │ ├── build.gradle.kts
│ │ └── src/
│ ├── android-app/ # Android-приложение
│ │ ├── build.gradle.kts
│ │ └── src/
│ └── web-client/ # Kotlin/JS клиент
│ ├── build.gradle.kts
│ └── src/
├── scripts/ # Вспомогательные скрипты
│ └── deploy.sh
└── docs/ # Документация
└── architecture.md
Файл settings.gradle.kts подключает модули:
rootProject.name = "my-kotlin-monorepo"
include(":modules:common")
include(":modules:backend")
include(":modules:android-app")
include(":modules:web-client")
В корневом build.gradle.kts можно задать общие плагины и зависимости:
plugins {
kotlin("jvm") version "1.9.22" apply false
kotlin("multiplatform") version "1.9.22" apply false
id("com.android.application") version "8.2.0" apply false
}
Управление зависимостями: Version Catalog и Convention Plugins
В монорепозитории критически важно избегать дублирования версий зависимостей. Для этого в Kotlin используют Version Catalog (файл libs.versions.toml).
Пример gradle/libs.versions.toml:
[versions]
kotlin = "1.9.22"
ktor = "2.3.7"
coroutines = "1.8.0"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
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-core = { 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 можно использовать:
dependencies {
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.kotlinx.coroutines.core)
}
Для сложных проектов полезно создать Convention Plugins — Gradle-плагины, которые настраивают общие конфигурации (например, компиляцию, тестирование, линтинг). Это позволяет избежать дублирования кода в каждом модуле.
Пример convention-плагина для JVM-модуля:
// buildSrc/src/main/kotlin/jvm-common.gradle.kts
plugins {
kotlin("jvm")
kotlin("plugin.serialization")
}
kotlin {
jvmToolchain(17)
}
dependencies {
implementation(libs.kotlin.stdlib)
testImplementation("org.jetbrains.kotlin:kotlin-test")
}
Тогда в backend/build.gradle.kts достаточно написать:
plugins {
id("jvm-common")
}
dependencies {
implementation(libs.ktor.server.core)
}
Работа с Gradle и CI/CD в монорепозитории
Одна из главных задач — настроить сборку так, чтобы изменения в одном модуле не пересобирали весь проект. Для этого используют Gradle Build Cache и инкрементальную компиляцию.
Пример оптимизированного CI-пайплайна (GitHub Actions):
name: Build Monorepo
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions