Монорепозиторий с Kotlin: организация кода на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: зачем монорепозиторий в 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. Они позволяют кешировать результаты сборки неизменённых модулей

Блоги

Рекомендаций по книгам