Separating Dependencies for Gradle Multi-Module Spring Boot Applications
Project Hierarchy
.
├── acme-utils
│ ├── build.gradle.kts
│ └── src
├── acme-domain
│ ├── build.gradle.kts
│ └── src
├── acme-repository
│ ├── build.gradle.kts
│ └── src
├── acme-security
│ ├── build.gradle.kts
│ └── src
├── acme-app-runner
│ ├── build.gradle.kts
│ └── src
├── buildSrc
│ ├── build
│ ├── build.gradle.kts
│ └── src
│ └── main
│ └── kotlin
│ └── com.acme.common-conventions.gradle.kts
├── gradle
│ └── wrapper
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
Shared Convention Plugin Configuration
The file buildSrc/src/main/kotlin/com.acme.common-conventions.gradle.kts establishes the baseline setup for all subprojects.
plugins {
`java-library`
`maven-publish`
id("org.springframework.boot")
}
apply(plugin = "io.spring.dependency-management")
tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
mainClass.set("com.acme.MainApplication")
}
repositories {
mavenLocal()
flatDir {
dirs("lib")
}
maven {
url = uri("https://maven.aliyun.com/repository/public/")
}
maven {
url = uri("https://oss.sonatype.org/content/repositories/snapshots/")
}
maven {
url = uri("https://repo.jenkins-ci.org/releases")
}
maven {
url = uri("https://repo.maven.apache.org/maven2/")
}
}
dependencies {
// shared dependencies
}
group = "com.acme"
version = "2.0.0"
java.sourceCompatibility = JavaVersion.VERSION_17
publishing {
publications.create<MavenPublication>("maven") {
from(components["java"])
}
}
tasks.withType<JavaCompile>() {
options.encoding = "UTF-8"
}
Entry Point Module Setup
The acme-app-runner/build.gradle.kts module handles the executable artifact and isolates the library dependencies.
plugins {
id("com.acme.common-conventions")
}
val depsDir = layout.buildDirectory.dir("dependencies")
// Task to isolate runtime classpath jars
tasks.register<Copy>("isolateLibs") {
delete(depsDir)
from(configurations.runtimeClasspath)
into(depsDir)
}
tasks.named<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
excludes.set(listOf("*.jar"))
dependsOn("isolateLibs")
manifest {
attributes(
"Class-Path" to configurations.runtimeClasspath.get().files.joinToString(" ") { "dependencies/${it.name}" }
)
}
}
dependencies {
implementation(project(":acme-utils"))
implementation(project(":acme-domain"))
implementation(project(":acme-repository"))
implementation(project(":acme-security"))
}
description = "Main Application Runner"
Build Output and Caveats
Executing the bootJar task will generate the primary executable archive and place all external libraries into the build/dependencies directory.
A notable drawback of this configurasion is that intra-project modules are packaged as *-plain.jar archives, meaning they do not bundle their own transitive dependencies. When deploying patches, simply swapping out a module's plain jar works if only internal logic changed. However, introducing or removing an external library dependency in a module requires manually updating the isolated dependency folder.