DEV Community

Joao Marcos Costa Salles
Joao Marcos Costa Salles

Posted on

Creating a modular Kotlin project using gradle.

Introduction

The idea of building a system entirely composed of microservices is always tempting for a developer, whether for the challenge itself or to stay ahead of scaling needs. However, analyzing trade-offs and choosing what best fits our context is also part of our profession.
A very common decision today at the beginning of a project is to build modular projects as a way of anticipating a future need to split out a dedicated service. This approach adds less initial complexity than a microservices architecture, while keeping modules sufficiently independent for a potential future separation if needed.

In this article, I will show hot to set up Gradle to enable modules on a monolithic project.

my-inflation/src/main/kotlin/com/salles/scrapping
├── data        # DTO declarations
├── db
│   ├── tables  # Persistence table declarations
├── domain      # Business logic declarations
├── repositories
├── routes
├── services
└── scrapper    # Feature that could move to another project
Enter fullscreen mode Exit fullscreen mode

To a modular monolith:

my-inflation
├── gradle
│   └── libs.versions.toml
├── myInflation  # Module to list prices and products
│   ├── src
│   └── build.gradle.kts
├── root         # Module to link other modules and set up Ktor
│   ├── src
│   └── build.gradle.kts
├── scrapper     # Module for scraping, which may move in the future
│   ├── src
│   └── build.gradle.kts
├── domain       # Module for domain declarations
│   ├── src
│   └── build.gradle.kts
├── data         # Module for the persistence layer
│   ├── src
│   └── build.gradle.kts
├── build.gradle.kts
└── settings.gradle.kts
Enter fullscreen mode Exit fullscreen mode

Module Declaration

It is necessary to declare the modules in settings.gradle.kts. In my case:

include(":myInflation", ":root", ":scrapper", ":data", ":domain")

If you use the same name as the folder, Gradle will find it automatically. If for some reason you need a different name, you can map it explicitly:

project(":myInflation").projectDir = file("core")

Shared Version Catalog

It is very important to create a libs.versions.toml file in the folder structure shown above. In it, we will maintain the versions of all libraries, ensuring consistency across all modules while gaining the benefits of caching and reducing build size. The file will look like this:

[versions]
kotlin = "2.3.20"
kotlinx-serialization = "1.9.0"

[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }

[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
Enter fullscreen mode Exit fullscreen mode

Module Dependency Configuration

Configuring dependencies between modules is straightforward. I will configure only the root module here, but other modules may also depend on the domain module, for example, and will need their own declarations.
Inside root/build.gradle.kts, add:

dependencies {
    implementation(project(":domain"))
    implementation(project(":data"))
    implementation(project(":myInflation"))
    implementation(project(":scrapper"))
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Only the root module contains the server runner, so add:

application {
    mainClass = "io.ktor.server.netty.EngineMain"
}
Enter fullscreen mode Exit fullscreen mode

Now we need to define which function should run to start the server. In root/src/main/resources/application.yaml:

ktor:
  application:
    modules:
      - com.salles.root.ApplicationKt.module
Enter fullscreen mode Exit fullscreen mode

Removing Unnecessary Dependencies

Finally, we can trim each module's dependencies. To do so, we will use the dependency-analysis-gradle-plugin to analyze module dependencies.

In libs.versions.toml, add the plugin version:

[versions]
# ...
dependency-analysis = "3.13.0"

[plugins]
# ...
dependency-analysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependency-analysis" }
Enter fullscreen mode Exit fullscreen mode

In build.gradle.kts, add:
subprojects {
apply(plugin = "com.autonomousapps.dependency-analysis")
}

To run the analysis:

./gradlew <module>:projectHealth
Enter fullscreen mode Exit fullscreen mode

Check {module}/build/reports/dependency-analysis for the report and change it if it makes sense.

With this, the project is configured for a modular structure. However, simply moving files is not enough — inter-module dependencies will still exist. It is necessary to define business logic that is exclusive to each feature module, but that is outside the scope of this article.

The dependency graph ends up looking like this. Note that scrapper and myInflation are now separated, with not direct dependency between the,:

Dependency Diagram

References:

my-inflation: https://github.com/JoaoSalles/my-inflation-api

Top comments (0)