Multi-project builds

Gradle · Practical · 12 min

Real codebases have many modules; structuring them well keeps builds fast and dependencies honest.

A multi-project build has one settings.gradle.kts that includes several subprojects, each with its own build.gradle.kts in its folder:

rootProject.name = "shop"
include("core", "api", "app")

Subprojects are addressed by path: :core, :services:billing for nested folders. Run a task in one project with ./gradlew :api:test; running ./gradlew test from the root runs it in every project that has it.

Projects depend on each other with implementation(project(":core")). Gradle then builds core first and uses its jar (with compile avoidance when only internals changed). Keep the graph acyclic.

Sharing configuration: the old subprojects { } / allprojects { } blocks in the root script are discouraged because they cross project boundaries and break with isolated projects and the configuration cache. The recommended way is a convention plugin: a small precompiled script plugin in buildSrc/src/main/kotlin/acme.java-conventions.gradle.kts (or an included build under build-logic/) that applies java, sets the toolchain, test settings and common dependencies. Each subproject then just writes plugins { id("acme.java-conventions") }.

A gradle.properties at the root holds shared properties such as version and JVM settings. ./gradlew projects prints the project tree. Composite builds (includeBuild("../lib")) let you substitute a published dependency with a local source checkout.

Example

// settings.gradle.kts
rootProject.name = "shop"
include("core", "api", "app")

// buildSrc/build.gradle.kts
// plugins { `kotlin-dsl` }
// repositories { mavenCentral() }

// buildSrc/src/main/kotlin/acme.java-conventions.gradle.kts
plugins { java }
repositories { mavenCentral() }
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test { useJUnitPlatform() }

// api/build.gradle.kts
plugins { id("acme.java-conventions"); `java-library` }
dependencies { api(project(":core")) }

// app/build.gradle.kts
plugins { id("acme.java-conventions"); application }
dependencies { implementation(project(":api")) }
application { mainClass = "com.acme.App" }
// $ ./gradlew :app:run      $ ./gradlew projects

Remember

  • include(...) in settings defines subprojects, addressed as :name
  • implementation(project(":core")) depends on a sibling
  • Share setup with convention plugins in buildSrc or build-logic, not subprojects { }
  • ./gradlew :api:test runs a task in one project

Watch out for

  • Configuring subprojects from the root with subprojects { }, which breaks modern features like the configuration cache.

Check yourself

  1. What is the recommended way to apply the same JDK toolchain and test setup to ten subprojects?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.gradle.org/current/userguide/multi_project_builds.html