Introduction: Why a Monorepo in Kotlin?
\nIn modern Kotlin development, a dilemma often arises: should you store several related projects (backend, microservices, libraries, Android apps) in separate repositories (polyrepo) or combine them into a single monorepo? A monorepo is a code storage strategy where all components reside in a single version control system. For Kotlin projects, this is especially relevant thanks to the powerful Gradle build system and built-in multi-module support.
\nIn this article, we will explore the practical aspects of organizing a monorepo with Kotlin: from directory structure to CI/CD configuration and dependency management. You will learn how to avoid common mistakes and get the maximum benefit from a single code repository.
\n\n1. Monorepo Structure: From Theory to Practice
\nProper structure is the foundation of a successful monorepo. Let's consider a typical project that includes a REST API on Ktor, a database library, and an Android app. Here is the recommended hierarchy:
\nmy-monorepo/\n├── build.gradle.kts # Root build file\n├── settings.gradle.kts # Module configuration\n├── gradle.properties # Common properties\n├── gradle/\n│ └── libs.versions.toml # Version Catalog\n├── libs/ # Shared libraries\n│ ├── common-utils/ # Utilities, extensions\n│ ├── common-db/ # Database work (Exposed)\n│ └── common-dto/ # Data Transfer Objects\n├── services/ # Server applications\n│ ├── user-service/ # User microservice\n│ └── order-service/ # Order microservice\n└── apps/ # Client applications\n └── android-app/ # Android app\n\nThe key element is settings.gradle.kts. It includes all modules:
rootProject.name = "my-monorepo"\n\ninclude("libs:common-utils")\ninclude("libs:common-db")\ninclude("libs:common-dto")\ninclude("services:user-service")\ninclude("services:order-service")\ninclude("apps:android-app")\n\nThis structure makes it easy to reuse code between services and the client, as well as centrally manage dependency versions.
\n\n1.1. Version Catalog: Dependency Management
\nIn a monorepo, it is critical to avoid duplicating library versions. Use the Version Catalog (file gradle/libs.versions.toml):
[versions]\nkotlin = "2.0.21"\nktor = "3.0.3"\nexposed = "0.57.0"\n\n[libraries]\nktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "ktor" }\nexposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }\n\n[plugins]\nkotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }\n\nNow in any module you can write:
\ndependencies {\n implementation(libs.ktor.server.core)\n implementation(libs.exposed.core)\n}\n\nThis ensures that all modules use the same versions and simplifies mass updates.
\n\n2. Practical Module Organization: Gradle Multi-Project
\nEach module in a monorepo is a separate Gradle subproject with its own build.gradle.kts. Let's look at the configuration of the common-utils library:
// libs/common-utils/build.gradle.kts\nplugins {\n alias(libs.plugins.kotlin.jvm)\n}\n\ngroup = "com.mycompany.common"\nversion = "1.0.0"\n\ndependencies {\n implementation(kotlin("stdlib"))\n implementation(libs.kotlinx.coroutines)\n}\n\nAnd here is a typical microservice on Ktor:
\n// services/user-service/build.gradle.kts\nplugins {\n alias(libs.plugins.kotlin.jvm)\n alias(libs.plugins.ktor)\n}\n\ndependencies {\n implementation(project(":libs:common-utils"))\n implementation(project(":libs:common-dto"))\n impl