Monorepository with Kotlin: Code Organization and Best Practices

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

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.sh


The 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.

Blogs

Book Recommendations