Spring Multi-Module (Gradle)
Spring Multi-Module (Gradle)
When several Spring applications live in a single repository, we have to settle the location of shared code, the build unit, and the direction of dependencies. Gradle's multi-project build is the standard tool for handling this kind of structure.
1. About Gradle multi-project
Gradle itself was started by Hans Dockter, first released publicly in 2007, and reached 1.0 in 2012. Multi-project builds are one of Gradle's core features — several subprojects sit under a root project and each is built as an independent module.
The core file set:
| File | Role |
|---|---|
settings.gradle(.kts) |
Sets the root name and registers subprojects with include. |
Root build.gradle(.kts) |
Applies common configuration via subprojects {} · allprojects {}. |
<module>/build.gradle(.kts) |
Per-module dependencies and plugins. |
Spring Modulith is a library released by the Spring team in 2022 that manages and verifies module boundaries at the package level inside a single application. It is a different flavor from multi-project builds.
2. include in settings.gradle
rootProject.name = 'platform'
include 'common'
include 'foo-api'
include 'bar-api'
Each include-ed directory is compiled, tested, and packaged as an independent module. Dependencies between subprojects are expressed as project references like dependencies { implementation project(':common') }.
3. subprojects block
In the root build.gradle we can apply common configuration to every subproject at once.
subprojects {
apply plugin: 'java'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories { mavenCentral() }
tasks.withType(Test) { useJUnitPlatform() }
}
allprojects includes the root, subprojects excludes it. The Spring Boot plugin is usually applied only to runnable modules that need bootJar, while library modules apply only io.spring.dependency-management — that's the typical separation.
4. The common + service-api pattern
A common shape when several services grow inside the same repository:
platform/
├── settings.gradle
├── build.gradle # subprojects common
├── common/ # domain-agnostic utilities, exceptions, DTO bases
│ └── build.gradle
├── foo-api/ # service A runnable module (bootJar)
│ └── build.gradle
└── bar-api/ # service B runnable module (bootJar)
└── build.gradle
common is built as a library jar, and *-api modules are built as runnable boot jars. The dependency direction is one-way — *-api → common. The reverse (common → *-api) breaks the module boundary.
5. Comparison with Maven multi-module
| Item | Gradle | Maven |
|---|---|---|
| First release | 2007 | 2004 (1.0) |
| Build script | Groovy/Kotlin DSL | XML (pom.xml) |
| Multi-module declaration | settings.gradle include |
Parent pom.xml <modules> |
| Build cache | Build/configuration cache built-in | Not provided by default |
| Parallel build | Supported by default | -T option |
Maven is declarative and convention-driven, while Gradle offers script-based flexibility. Saying one is absolutely superior is hard — the option the team is more familiar with usually has lower operational cost.
6. Spring Modulith and a single module
Spring Modulith treats packages as modules inside a single application jar and verifies and documents module dependencies. If multi-project is about separating build units, Modulith is the approach where the runtime is one piece while code boundaries are enforced. The two can coexist.
For a small service, not splitting modules and just separating by package can be operationally simpler. Module separation is a tradeoff between gains in build time, reusability, and ownership separation against the configuration complexity it adds.
7. Module split criteria
- By domain —
order-api·payment-api·common. Natural when service units are clear. - By technical layer —
web·domain·infrastructure. Often used in hexagonal or clean architecture variants.
Splitting by domain and layer simultaneously explodes the module count. Usually we cut by domain first and split layers only inside large domains.
8. Version catalogs
Gradle 7.0+ provides a standard mechanism for managing dependency versions.
# libs.versions.toml
[versions]
spring-boot = "3.4.1"
[libraries]
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
When several modules share the same version, we can treat it as a single source.
9. Common pitfalls
Cyclic dependency — a → b → a. Gradle blocks this at build time, but it can be worked around in unintended ways (for example, lifting an interface up to common).
Unintended bootJar application — applying the Spring Boot plugin as-is to a library module can prevent a regular jar from being produced. We make the bootJar { enabled = false } and jar { enabled = true } combination explicit.
api vs implementation — dependencies exposed via api propagate to downstream projects. To keep the surface small, we default to implementation.
Sharing test fixtures — for sharing test helpers across modules, the java-test-fixtures plugin provides a standardized way.
Closing thoughts
Bringing multi-module into a small service makes the build and configuration burden grow faster than the code itself. It is safer to introduce it once two or more domains are clearly distinct. Once it settles, the gains from build cache, parallel builds, and ownership separation become visible.
Next
- spring-webflux-vs-mvc
See Gradle Multi-Project Builds · Gradle Version Catalogs · Spring Boot Gradle Plugin · Spring Modulith · Maven Multi-Module Project · Java Test Fixtures.