Structuring Projects with Gradle
A multi-project build in Gradle consists of one root project and one or more subprojects.

Gradle can build the root project and any number of the subprojects in a single execution.
Project locations
Multi-project builds are represented by a tree with a single root. Each element in the tree represents a project.
Project and subproject are used interchangeably in this section. |
A project has a path, which denotes the position of the project in the multi-project build tree.
In most cases, the project path is consistent with its location in the file system. However, this behavior is configurable if necessary.
The project tree is created in the settings.gradle(.kts)
file.
The location of the settings file is also the location of the root project.
A simple build
Let’s look at a basic multi-project build example that contains a root project and a single subproject.
The subproject is called app
:
.
├── app
│ ...
│ └── build.gradle.kts
└── settings.gradle.kts
.
├── app
│ ...
│ └── build.gradle
└── settings.gradle
This is the recommended project structure for starting any Gradle project. The build init plugin also generates skeleton projects that follow this structure - a root project with a single subproject:
rootProject.name = "basic-multiproject"
include("app")
rootProject.name = 'basic-multiproject'
include 'app'
In this case, Gradle will look for a build file in the app
directory.
We can view the structure of a multi-project build by running the gradle projects
command:
$ gradle -q projects ------------------------------------------------------------ Root project 'basic-multiproject' ------------------------------------------------------------ Root project 'basic-multiproject' \--- Project ':app' To see a list of the tasks of a project, run gradle <project-path>:tasks For example, try running gradle :app:tasks
In the example below, the app
subproject is a Java application that applies the application plugin and configures the main class accordingly:
plugins {
id("application")
}
application {
mainClass = "com.example.Hello"
}
plugins {
id 'application'
}
application {
mainClass = 'com.example.Hello'
}
package com.example;
public class Hello {
public static void main(String[] args) {
System.out.println("Hello, world!");
}
}
We can then run the application by executing the run
task from the application plugin.
$ gradle -q run Hello, world!
Building the tree
In the settings file, you can use the include
method to define the project tree:
include("project1", "project2:child1", "project3:child1")
include 'project1', 'project2:child1', 'project3:child1'
The include
method takes project paths as arguments.
The project path is assumed to be equal to the relative physical file system path.
For example, a path services:api
is mapped by default to a folder ./services/api
(relative to the project root .
).
You only need to specify the leaves of the tree.
This means that including the path services:hotels:api
will create 3 projects: services
, services:hotels
, and services:hotels:api
.
More examples of how to work with the project path can be found in the DSL documentation of Settings.include(java.lang.String[]).
Logical vs. Physical paths
You should avoid creating intermediate projects by changing the directory of included projects as needed:
include("/my/custom/path/subproject")
The physical and logical structure and location of projects (i.e, subprojects, modules) do not have to be identical.
A subproject located on disk at subs/web/my-web-module
can have a logical name of :my-web-module
or :subs:web:my-web-module
depending on the settings.gradle(.kts)
file:
include("my-web-module") // :my-web-module
include("subs/my-web-module") // :subs:web:my-web-module
Adding subprojects
Let’s add another subproject called lib
to the previously created project.
All we need to do is add another include
statement in the root settings file:
rootProject.name = "basic-multiproject"
include("app")
include("lib")
rootProject.name = 'basic-multiproject'
include 'app'
include 'lib'
Gradle will then look for the build file of the new lib
subproject in the ./lib/
subdirectory of the project:
.
├── app
│ ...
│ └── build.gradle.kts
├── lib
│ ...
│ └── build.gradle.kts
└── settings.gradle.kts
.
├── app
│ ...
│ └── build.gradle
├── lib
│ ...
│ └── build.gradle
└── settings.gradle
Using buildSrc
for build logic
Complex build logic is a good candidate for being encapsulated as a custom task or binary plugin. Custom tasks and plugin implementations should not live in the build script.
buildSrc
is a Gradle-recognized and protected directory for managing custom build logic and shared configuration among subprojects.
It is ideal for custom plugins and custom tasks.
It’s also great for keeping build scripts clean and implementation separate from declaration.
The buildSrc
directory is treated as an included build.
Upon discovering the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script.
For multi-project builds, there can be only one buildSrc
directory, which has to sit in the root project directory.
The downside of using buildSrc is that any change to it will cause every task in your project to be invalidated and have to rerun.
|
buildSrc
uses the same source code conventions applicable to Java, Groovy, and Kotlin projects.
It also provides direct access to the Gradle API.
Additional dependencies can be declared in a dedicated build.gradle(.kts)
under buildSrc
.
repositories {
mavenCentral()
}
dependencies {
testImplementation("junit:junit:4.13")
}
repositories {
mavenCentral()
}
dependencies {
testImplementation 'junit:junit:4.13'
}
A typical project including buildSrc
has the following layout:
.
├── buildSrc
│ ├── build.gradle.kts
│ └── src
│ ├── main
│ │ └── java
│ │ └── com
│ │ └── enterprise
│ │ ├── Deploy.java
│ │ └── DeploymentPlugin.java
│ └── test
│ └── java
│ └── com
│ └── enterprise
│ └── DeploymentPluginTest.java
├── settings.gradle.kts
├── subproject-one
│ └── build.gradle.kts
└── subproject-two
└── build.gradle.kts
.
├── buildSrc
│ ├── build.gradle
│ └── src
│ ├── main
│ │ └── java
│ │ └── com
│ │ └── enterprise
│ │ ├── Deploy.java
│ │ └── DeploymentPlugin.java
│ └── test
│ └── java
│ └── com
│ └── enterprise
│ └── DeploymentPluginTest.java
├── settings.gradle
├── subproject-one
│ └── build.gradle
└── subproject-two
└── build.gradle
Adding buildSrc
Let’s add buildSrc
to the previously created project and move common configuration to buildSrc/src/main/kotlin
or buildSrc/src/main/groovy
:
.
├── app
│ ...
│ └── build.gradle.kts
├── lib
│ ...
│ └── build.gradle.kts
├── buildSrc
│ ├── build.gradle.kts
│ └── src/main/kotlin/shared-build-configurations.gradle.kts
└── settings.gradle.kts
.
├── app
│ ...
│ └── build.gradle
├── lib
│ ...
│ └── build.gradle
├── buildSrc
│ ├── build.gradle
│ └── src/main/groovy/shared-build-configurations.gradle
└── settings.gradle
Gradle automatically compiles and tests the code in buildSrc
and puts it in the classpath of your build script:
object Conventions {
const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.21"
}
Which you can use accordingly:
dependencies {
implementation(Conventions.kotlinStdLib)
}
Modifying elements
The multi-project tree created in the settings file comprises 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.
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, which is 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:\
).