Monorepository with Kotlin: code organization and best practices
As projects grow and the number of services increases, developers face a question: how to organize code so that it remains manageable, easily testable, and quickly deployable? One popular approach is a monorepository (monorepo). In this article, we will look at how to effectively organize a monorepository in Kotlin using Gradle, which tools and patterns to apply, and how to avoid typical mistakes.
What is a monorepository and why is it needed?
A monorepository is an approach where the code of several projects (modules, libraries, services) is stored in one repository. In the context of Kotlin, this means you can have a common Gradle project containing modules for backend services, Android applications, shared libraries, and even documentation.
Advantages of a monorepository:
- Unified versioning — all modules use the same version of Kotlin and shared dependencies.
- Simplified refactoring — changes in a shared library are immediately visible to all consumers.
- Shared CI/CD configuration — one pipeline for all modules.
- Improved collaboration — developers see the entire project code.
However, a monorepository requires discipline: without proper organization, it can turn into a "monolith" with tangled dependencies. Kotlin and Gradle provide excellent tools to combat this.
Structure of a monorepository in Kotlin
Let's consider a typical project structure where there is a backend service on Ktor, a shared library for data models, and an Android application:
my-monorepo/├── build.gradle.kts # root build file├── settings.gradle.kts # module inclusion├── gradle.properties # shared versions├── 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.shThe settings.gradle.kts file includes modules:
rootProject.name = "my-monorepo"
include(":common")include(":backend")include(":android-app")The root build.gradle.kts usually contains only shared plugins and settings:
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"}Important: plugins are specified with apply false so they are not applied to the root project but are available to child modules.
Dependency management in a monorepository
One of the main tasks is to avoid duplicating dependency versions. For this, use Gradle Version Catalog (file libs.versions.toml in the gradle folder). Example:
# 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.