Multi-Project Builds
As projects grow, it’s common to split them into smaller, focused modules that are built, tested, and released together. Gradle supports this through multi-project builds, allowing you to organize related codebases under a single build while keeping each module logically isolated.
Multi-Project Layout
A multi-project build consists of a root project and one or more subprojects, all defined in a single settings.gradle(.kts)
file.
This structure supports modularization, parallel execution, and code reuse.

A typical multi-project structure looks like this:
my-project/
├── settings.gradle.kts (1)
├── build.gradle.kts (2)
├── app/ (3)
│ └── build.gradle.kts (4)
├── core/ (3)
│ └── build.gradle.kts (5)
└── util/ (3)
└── build.gradle.kts (6)
my-project/
├── settings.gradle (1)
├── build.gradle (2)
├── app/ (3)
│ └── build.gradle (4)
├── core/ (3)
│ └── build.gradle (5)
└── util/ (3)
└── build.gradle (6)
1 | Declares subprojects |
2 | Root project build logic (optional) |
3 | Subproject |
4 | App module |
5 | Shared core logic |
6 | Utility code |
Each subproject can define its own build logic, dependencies, and plugins.
In settings.gradle(.kts)
, you include subprojects by name using include()
.
The include()
method takes project paths as arguments:
rootProject.name = "my-project"
include("app", "core", "util")
rootProject.name = 'my-project'
include('app', 'core', 'util')
By default, a project path corresponds to the relative physical location of the project directory.
For example, the path services:api
maps to the directory ./services/api
, relative to the root project.
You can find more examples and detailed usage in the DSL reference for Settings.include(String…)
.
Project Descriptors
To further describe the project architecture to Gradle, the settings file provides project descriptors.
You can modify these descriptors in the settings file at any time.
To access a descriptor, you can:
include("project-a")
println(rootProject.name)
println(project(":project-a").name)
include('project-a')
println rootProject.name
println project(':project-a').name
Using this descriptor, you can change the name, project directory, and build file of a project:
rootProject.name = "main"
include("project-a")
project(":project-a").projectDir = file("custom/my-project-a")
project(":project-a").buildFileName = "project-a.gradle.kts"
rootProject.name = 'main'
include('project-a')
project(':project-a').projectDir = file('custom/my-project-a')
project(':project-a').buildFileName = 'project-a.gradle'
Consult the ProjectDescriptor class in the API documentation for more information.
Modifying a subproject path
Let’s take a hypothetical project with the following structure:
.
├── settings.gradle.kts
├── app/
│ ├── build.gradle.kts
│ └── src/
└── subs/ (1)
├── build.gradle.kts
└── web (1)
└── my-web-module (2)
.
├── settings.gradle
├── app/
│ ├── build.gradle
│ └── src/
└── subs/ (1)
├── build.gradle
└── web (1)
└── my-web-module (2)
1 | Gradle may see this as a subproject |
2 | Actual intended subproject |
If your settings.gradle(.kts)
looks like this:
include(':subs:web:my-web-module')
Gradle sees a subproject with a logical project name of :subs:web:my-web-module
and two, possibly unintentional, other subprojects logically named:
-
:subs
-
:subs:web
This can lead to phantom build directories, especially when using allprojects{}
or subproject{}
.
To avoid this, you can use:
include(':my-web-module')
project(':my-web-module').projectDir = "subs/web/my-web-module"
So that you only end up with a single subproject named :my-web-module
.
So, while the physical project layout is the same, the logical results are different.
Naming recommendations
As your project grows, naming and consistency get increasingly more important.
To keep your builds maintainable, we recommend the following:
-
Keep default project names for subprojects: It is possible to configure custom project names in the settings file. However, it’s an unnecessary extra effort for the developers to track which projects belong to what folders.
-
Use lower case hyphenation for all project names: All letters are lowercase, and words are separated with a dash (
-
) character. -
Define the root project name in the settings file: The
rootProject.name
effectively assigns a name to the build, used in reports like Build Scans. If the root project name is not set, the name will be the container directory name, which can be unstable (i.e., you can check out your project in any directory). The name will be generated randomly if the root project name is not set and checked out to a file system’s root (e.g.,/
orC:\
).
Declaring dependencies between Subprojects
What if one subproject depends on another? What if one subproject depends on the artifact produced by another?

This is a common use case in multi-project builds. Gradle supports this scenario with project dependencies.
Depending on another Project
Consider a multi-project build with the following layout:
.
├── api
│ ├── src
│ │ └──...
│ └── build.gradle.kts
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle.kts
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle.kts
└── settings.gradle.kts
.
├── api
│ ├── src
│ │ └──...
│ └── build.gradle
├── services
│ └── person-service
│ ├── src
│ │ └──...
│ └── build.gradle
├── shared
│ ├── src
│ │ └──...
│ └── build.gradle
└── settings.gradle
In this example:
-
person-service
depends on bothapi
andshared
-
api
depends onshared
You declare these relationships using the project path, which uses colons (:
) to indicate nesting.
For example:
-
:shared
refers to theshared
subproject -
services:person-service
refers to a nested subproject
rootProject.name = "dependencies-java"
include("api", "shared", "services:person-service")
plugins {
id("java")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
}
plugins {
id("java")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
implementation(project(":shared"))
}
plugins {
id("java")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
implementation(project(":shared"))
implementation(project(":api"))
}
rootProject.name = 'basic-dependencies'
include 'api', 'shared', 'services:person-service'
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation "junit:junit:4.13"
}
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation "junit:junit:4.13"
implementation project(':shared')
}
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation "junit:junit:4.13"
implementation project(':shared')
implementation project(':api')
}
For more details on project paths, consult the DSL documentation for Settings.include(String…).
A project dependency affects both the build order and classpath:
-
The required project will be built first.
-
Its compiled classes and transitive dependencies are added to the consuming project’s classpath.
For example, running ./gradlew :api:compileJava
will first build shared
, then api
.
Depending on Artifacts produced by another Project
Sometimes, you only need the output of a specific task from another project—not the entire project itself.
While you can create task-to-task dependencies between projects, Gradle discourages this because it creates tight coupling between tasks.
Instead, use outgoing artifacts to expose a task’s output and model it as a dependency. Gradle’s variant-aware dependency management allows one project to consume artifacts from another in a structured, on-demand way.