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
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
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" }
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"))
// ...
}
Only the root module contains the server runner, so add:
application {
mainClass = "io.ktor.server.netty.EngineMain"
}
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
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" }
In build.gradle.kts, add:
subprojects {
apply(plugin = "com.autonomousapps.dependency-analysis")
}
To run the analysis:
./gradlew <module>:projectHealth
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,:
References:
my-inflation: https://github.com/JoaoSalles/my-inflation-api














