A common tool used for starting a new Spring Project is the Spring Initializr. Using this, selecting Kotlin as your language, will create a simple Spring Boot project with a single build.gradle.kts
file.
However, as projects become bigger, we may want to split up our codebase into multiple build modules for better maintainability and understandability. But to understand why we would want a multi-module project, let’s first look at what a module actually is.
A module:
- has a separate codebase to other modules’ code,
- is transformed into a separate JAR file (artefact) during a build, and
- can implement different dependencies to other modules.
What is the Benefit to Modular Programming?
“The benefits of using modular programming include:
- less code has to be written.
- a single procedure can be developed for reuse, eliminating the need to retype the code many times.
- the code is stored across multiple files.
- errors can easily be identified, as they are localised to a subroutine or function.
- the same code can be used in many applications.
- the scoping of variables can easily be controlled.”
If we split up the codebase into multiple smaller modules that each has clearly defined dependencies to other modules, we take a big step towards an easily maintainable codebase.
Let’s now look at an example of how this can be implemented. For this we will imagine an example Spring Boot project with a backend
module and a test-utils
module. We want the backend
to use the test-utils
module. The folder structure would look as follows:
├── backend
| ├── src
| └── build.gradle.kts
├── test-utils
| ├── src
| └── build.gradle.kts
├── build.gradle.kts
└── settings.gradle.kts
Each module is in a separate folder with Kotlin sources, a build.gradle.kts
file
The top-level build.gradle.kts
file configures build behaviour that is shared between all sub-modules so that we don’t have to duplicate things in the sub-modules.
The backend
module contains the actual Spring Boot application.
The test-utils
module provides certain util classes that can be accessed by our backend
module.
Let’s now take a look at how these files may look.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
buildscript {
repositories {
mavenCentral()
}
}
plugins {
id("org.springframework.boot") version "2.2.0.RELEASE" apply false
id("io.spring.dependency-management") version "1.0.8.RELEASE" apply false
kotlin("jvm") version "1.3.50" apply false
kotlin("plugin.spring") version "1.3.50" apply false
}
allprojects {
group = "com.example"
version = "1.0.0"
tasks.withType<JavaCompile> {
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "1.8"
}
}
}
subprojects {
repositories {
mavenCentral()
}
apply {
plugin("io.spring.dependency-management")
}
}
plugins {
kotlin("jvm")
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
}
Notice we don’t need to specify the version of the dependencies in this gradle file as they are stated in the parent gradle file.
plugins {
id("org.springframework.boot")
kotlin("jvm")
kotlin("plugin.spring")
}
dependencies {
implementation(project(":test-utils"))
implementation(kotlin("reflect"))
implementation(kotlin("stdlib-jdk8"))
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
Note the line implementation(project(":test-utils"))
. This is very powerful as it allows for you to pull in this module as a dependency.
Just be aware of circular dependencies, i.e. modules that depend on each other. You cannot also have implementation(project(":backend"))
in the test-utils
module as this would create a circular dependency.
Lastly then you must tell the project that the modules exist and should be included, this is done in the setting.gradle.kts
:
rootProject.name = "example"
include("backend", "test-utils")
This example shows a basic set up of a multi-module project using the Kotlin DSL for the build files.
To run gradle task on a specific module you can do so using the following idea:
./gradlew :backend:clean :backend:build
This can be useful for testing or running only one module at a time.