5 Steps to Make Gradle Configuration Extreme Clean in a Multi-Module Project¶
Multi-module Gradle projects involve numerous tasks during the build process. Managing dependency version control, plugin usage, build logic, and more with Gradle proves to be a popular and effective approach. But, achieving these tasks requires a lot of configuration scripts, which can make the file more complicated, and more difficult for development. These steps in the article will guide you through a clean and efficient way to manage configuration files:
- extract version declaring in gradle.properties.
- define all plugins and repositories in settings.gradle.
- define all libraries in the allprojects.dependencyManagementin./build.gradle.
- declaring dependency and plugin directly instead of using subprojectin submodule.
- extract complex and common task config to extra files and apply wherever needed.
Take a look at this repository or refactor PR, if you can't wait to find out how it looks.
Step by Step Demonstration¶
Step 1: Extract Version Declaration¶
Version declarations can be extracted into a gradle.properties file. Additionally, Gradle arguments can be defined as shown below:
group='org.example'
version=0.0.1.SNAPSHOT
# Plugin Version
jibVersion=3.4.3
# Spring Version
springBootVersion=3.1.5
springDependencyVersion=1.1.4
springCloudVersion=2022.0.1
# Dependency Version
springdocVersion=2.1.0
feignMicrometerVersion=12.1
wiremockVersion=3.7.0
logbackAppenderVersion=1.4.0-rc2
lombokVersion=1.18.20
# Gradle Argument
org.gradle.parallel=true
Step 2: Define Used Plugins and Maven Source¶
All used plugins and the source Maven repository can be defined in a settings.gradle:
import org.gradle.api.initialization.resolve.RepositoriesMode
pluginManagement {
    plugins {
        id 'org.springframework.boot' version "${springBootVersion}"
        id 'io.spring.dependency-management' version "${springDependencyVersion}"
        id 'com.google.cloud.tools.jib' version "${jibVersion}"
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
        mavenLocal()
        maven {
            url '.m2/local/'
        }
    }
}
rootProject.name = 'event-sourcing-order-poc'
include 'modules'
include 'modules:common'
findProject(':modules:common')?.name = 'common'
// ... and other modules settings
include 'order'
include 'order:command-side'
findProject(':order:command-side')?.name = 'order-command-side'
include 'order:event-handler'
findProject(':order:event-handler')?.name = 'order-event-handler'
include 'order:query-side'
findProject(':order:query-side')?.name = 'order-query-side'
// ... and other sub-project settings
Step 3: Define Allprojects DependencyManagement¶
All the used libraries should be defined in a allprojects.dependencyManagement closure in build.gradle of the root module:
import org.springframework.boot.gradle.plugin.SpringBootPlugin
plugins {
    id 'java'
    id 'java-library'
    id 'io.spring.dependency-management'
    id 'org.springframework.boot' apply false
    id 'com.google.cloud.tools.jib' apply false
}
allprojects {
    java {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
    apply plugin: 'java'
    apply plugin: 'io.spring.dependency-management'
    apply plugin: 'java-library'
    dependencyManagement {
        imports {
            mavenBom SpringBootPlugin.BOM_COORDINATES
            mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
        }
        dependencies {
            dependency "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
            dependency "io.github.openfeign:feign-micrometer:${feignMicrometerVersion}"
            dependency "org.projectlombok:lombok:${lombokVersion}"
            dependency "org.wiremock:wiremock:${wiremockVersion}"
            dependency "com.github.loki4j:loki-logback-appender:${logbackAppenderVersion}"
        }
    }
    dependencies {
        // only declare all-needed dependencies
        compileOnly "org.projectlombok:lombok:${lombokVersion}"
        annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
    }
    test {
        useJUnitPlatform()
    }
}
tasks.named("jar") {
    enabled = false
}
in the dependencyManagement closure, we can first import the BOM of other dependencies project like spring-boot-dependencies and spring-cloud-dependencies. Then, we can declare the version of other used libraries.
Step4: Avoid Using subprojects {}¶
Declaring dependency and plugin directly instead of using subproject in build.gradle for sub-modules like:
plugins {
    id 'org.springframework.boot'
    id 'com.google.cloud.tools.jib'
}
apply from: "$rootDir/gradle/jib.gradle"
dependencies {
    implementation project(":modules:common")
    implementation project(":modules:event")
    implementation project(":modules:client")
    implementation project(":modules:observation")
    implementation project(":modules:idempotency")
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.kafka:spring-kafka'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.junit.jupiter:junit-jupiter-api'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
It can be more intuitive to declare the used plugin and dependencies in each project. Thanks to the dependencyManagement in the root module, we can use a simple form of the declaration here in the subproject.
Step 5: Extract Related Configuration¶
Extract complex and common task config to extra files and apply them wherever needed.
In the above file ./order/command-side/build.gradle, the important script snippet 
will include an extra .gradle file, which we can group related config into one file. Let's take the ./gradle/jib.gradle for example:
jib {
    from {
        image = "openjdk:17-slim"
    }
    to.image = "noahhsu/${project.name}"
    to.tags = ["latest"]
    container {
        creationTime = 'USE_CURRENT_TIMESTAMP'
    }
}
In this way, we can make the .gradle file in the submodules/subprojects is very clean and more readable. Moreover, we can reuse these configurations in different places (e.g. order/query-side, payment/command-side, etc.).
Summary¶
In conclusion, managing a multi-module Gradle project can be streamlined and elegant by adopting a structured approach to configuration. In this article, we propose a five-step method to centralize plugin and dependency version declarations and extract configurations into independent .gradle files. Besides, be cautious when using special methods to ensure the project-building logic straightforward and easy to manage. By following these steps, you can enhance the readability and maintainability of your multi-module Gradle projects.
 
                    