It is important to structure your Gradle project to optimize build performance. A multi-project build is the standard in Gradle.

Project Concepts

There are four key concepts to understand Gradle projects:

  1. Root project: The top-level project in a build that contains the settings.gradle(.kts) file and typically aggregates all subprojects.

  2. Subprojects: Individual modules (components) that are part of a multi-project build and are included by the root project via the settings.gradle(.kts) file.

  3. Settings file: A settings.gradle(.kts) configuration file used to define the structure of a multi-project build, including which subprojects are part of it and optionally how they’re named or located.

  4. Build scripts: build.gradle(.kts) files that define how a project is built (applying plugins, declaring dependencies, configuring tasks, etc…​) executed per project (subprojects can each have one).

Let’s take a look at an example:

my-project/     (1)
├── settings.gradle.kts (2)
├── app/                    (3)
│   ├── build.gradle.kts        (4)
│   └── src/
├── core/                   (3)
│   ├── build.gradle.kts        (4)
│   └── src/
└── util/                   (3)
    ├── build.gradle.kts        (4)
    └── src/
my-project/     (1)
├── settings.gradle (2)
├── app/                (3)
│   ├── build.gradle        (4)
│   └── src/
├── core/               (3)
│   ├── build.gradle        (4)
│   └── src/
└── util/               (3)
    ├── build.gradle        (4)
    └── src/
1 Root project directory
2 Settings file
3 Subproject
4 Subproject build file

Single-Project Build

structuring builds 1

Let’s look at a basic multi-project build example that contains a root project and a single subproject.

The root project is called my-project, located somewhere on your machine. From Gradle’s perspective, the root is the top-level directory ..

The project contains a single subproject called app:

.   (1)
├── settings.gradle (2)
└── app/                (3)
    ├── build.gradle       (4)
    └── src/            (5)
.   (1)
├── settings.gradle.kts (2)
└── app/                    (3)
    ├── build.gradle.kts        (4)
    └── src/                (5)
1 Root project
2 Subproject
3 Subproject build file
4 Settings file
5 Source code and more

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:

The settings.gradle(.kts) file describes the project structure to Gradle:

settings.gradle.kts
rootProject.name = "my-project"
include("app")
settings.gradle
rootProject.name = 'my-project'
include 'app'

In this case, Gradle will look for a build file for the app subproject in the ./app directory.

You can view the structure of a multi-project build by running the projects command:

$ ./gradlew -q projects

Projects:

------------------------------------------------------------
Root project 'my-project'
------------------------------------------------------------

Root project 'my-project'
\--- 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 this example, the app subproject is a Java application that applies the Java Application plugin and configures the main class.

The application prints Hello World to the console:

app/build.gradle.kts
plugins {
    id("application")
}

application {
    mainClass = "com.example.Hello"
}
app/build.gradle
plugins {
    id 'application'
}

application {
    mainClass = 'com.example.Hello'
}
app/src/main/java/com/example/Hello.java
package com.example;

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, world!");
    }
}

You can run the application by executing the run task from the Application plugin in the project root:

$ ./gradlew -q run
Hello, world!

Multi-Project Build (include())

structuring builds 2

In the settings file, you can use the include method to add another subproject to the root project:

settings.gradle.kts
include("project1", "project2:child1", "project3:child1")
settings.gradle
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 .).

More examples of how to work with the project path can be found in the DSL documentation of Settings.include(java.lang.String[]).

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:

settings.gradle.kts
rootProject.name = "my-project"
include("app")
include("lib")
settings.gradle
rootProject.name = 'my-project'
include 'app'
include 'lib'

Gradle will then look for the build file of the new lib subproject in the ./lib/ directory:

.       (1)
├── settings.gradle.kts (2)
├── app/                    (3)
│   ├── build.gradle.kts        (4)
│   └── src/
└── lib/                    (3)
    ├── build.gradle.kts        (4)
    └── src/
.       (1)
├── settings.gradle (2)
├── app/                (3)
│   ├── build.gradle        (4)
│   └── src/
└── lib/                (3)
    ├── build.gradle        (4)
    └── src/
1 Root project
2 Settings file
3 Subproject
4 Subproject build file

You can learn more about multi-project builds in Multi-Project Builds.

Sharing Build Logic (buildSrc)

structuring builds 3

When projects grow in size and complexity, it’s common to see the same logic repeated across multiple subprojects—like applying the same plugins, configuring the same tasks, or declaring the same dependencies.

Duplicated build logic is hard to maintain and easy to get wrong. Gradle provides a built-in way to centralize and reuse this logic: a special directory called buildSrc.

buildSrc is a separate build located in the root of your Gradle project. Any code you put in this directory is automatically compiled and added to the classpath of your main build.

Let’s take a look at our multi-project build:

.
├── settings.gradle.kts
├── app/
│   ├── build.gradle.kts    (1)
│   └── src/
└── lib/
    ├── build.gradle.kts    (1)
    └── src/
.
├── settings.gradle
├── app/
│   ├── build.gradle    (1)
│   └── src/
└── lib/
    ├── build.gradle    (1)
    └── src/
1 Subproject build script, applies java-library and testing logic

We now encapsulate reusable configuration logic in a java-library-convention.gradle.kts file.

Because the file is named java-library-convention.gradle.kts, Gradle automatically registers it as a plugin with the ID java-library-convention. It compiles and makes this plugin available to all other build scripts in the project.

.
├── settings.gradle.kts
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/main/kotin/
│       └── java-library-convention.gradle.kts  (1)
├── app/
│   ├── build.gradle.kts    (2)
│   └── src/
└── lib/
    ├── build.gradle.kts    (2)
    └── src/
.
├── settings.gradle
├── buildSrc/
│   ├── build.gradle
│   └── src/main/groovy/
│       └── java-library-convention.gradle  (1)
├── app/
│   ├── build.gradle    (2)
│   └── src/
└── lib/
    ├── build.gradle    (2)
    └── src/
1 Applies java-library and testing logic
2 Subproject build script, applies java-library and testing logic

You can learn more about multi-project builds in Sharing Build Logc between Subprojects using BuildSrc.

Composite Builds (includeBuild())

structuring builds 4

In Gradle, composite builds (or included builds) are ways to compose multiple builds together.

They allow you to work with multiple Gradle projects (builds) as if they were part of a single build, without needing to publish artifacts to a repository.

Imagine we want to break up our multi-project build into two separate builds:

  • A shared library in libs

  • An application that uses it (our previous multi-project build)

We want to:

  • Keep them in separate builds (they could be in separate repos)

  • But develop them together without publishing lib

.
├── settings.gradle.kts
├── buildSrc/
│   ├── build.gradle.kts
│   └── src/main/kotin/
│       └── java-library-convention.gradle.kts
├── app/
│   ├── build.gradle.kts
│   └── src/
├── core/
│   ├── build.gradle.kts
│   └── src/
├── util/
│   ├── build.gradle.kts
│   └── src/
└── libs/           (1)
    └── lib/
        ├── settings.gradle.kts
        ├── build.gradle.kts
        └── src/
.
├── settings.gradle
├── buildSrc/
│   ├── build.gradle
│   └── src/main/groovy/
│       └── java-library-convention.gradle
├── app/
│   ├── build.gradle
│   └── src/
├── core/
│   ├── build.gradle
│   └── src/
├── util/
│   ├── build.gradle
│   └── src/
└── libs/           (1)
    └── lib/
        ├── settings.gradle
        ├── build.gradle
        └── src/
1 Standalone, reusable library (included build)

Here, lib is a separate Gradle build that lives in libs/lib. It’s not part of the normal include(…​) multi-project structure.

Instead, we treat it as an included build, a distinct build that we can use without publishing it to a repository.

In the root settings.gradle(.kts), we tell Gradle to include lib as part of a composite build using includeBuild():

settings.gradle.kts
includeBuild("lib")
settings.gradle
includeBuild 'lib'

You can learn more about multi-project builds in Composite Builds.

Structuring Recommendations

Source code and build logic should be organized in a clear, consistent, and meaningful way. This section outlines recommendations that lead to readable and maintainable Gradle projects. It also highlights common pitfalls and how to avoid them to ensure your builds stay robust and scalable.

Use separate Language-specific Source Files

Gradle’s language plugins define conventions for discovering and compiling source code. For example, when the Java plugin is applied, Gradle automatically compiles source files in src/main/java.

Other language plugins follow a similar convention: the last part of the source directory (e.g., java, groovy, kotlin) indicates the language of the source files it contains.

Some compilers support cross-compilation of multiple languages from the same directory. For example, the Groovy compiler can compile both Java and Groovy source files from src/main/groovy.

However, Gradle recommends separating source files by language into distinct directories (e.g., src/main/java and src/main/kotlin). This improves build performance and makes builds more predictable—both for Gradle and for humans reading the project layout.

Here’s an example source layout for a project using both Java and Kotlin:

.
├── build.gradle.kts
└── src
    └── main
        ├── java
        │   └── HelloWorld.java
        └── kotlin
            └── Utils.kt
.
├── build.gradle
└── src
    └── main
        ├── java
        │   └── HelloWorld.java
        └── kotlin
            └── Utils.kt

Use separate Source Files per Test type

It’s common for a project to define and run multiple types of tests—such as unit tests, integration tests, functional tests, or smoke tests. To keep things maintainable and organized, Gradle recommends storing each test type’s source code in its own dedicated source directory.

For example, instead of placing all tests under src/test/java, you might use:

src/
├── test/                      // Unit tests
│   └── java/
├── integrationTest/           // Integration tests
│   └── java/
└── functionalTest/            // Functional tests
    └── java/

To see how this works in practice, check out the example project which demonstrates how to define a custom integrationTest source set and task in a Java-based project.

Gradle allows you to define multiple source sets and test tasks, so you can fully isolate and control each type of test in your build.

Use standard Conventions

All Gradle core plugins follow the convention over configuration principle, a well-known software engineering paradigm that favors sensible defaults over manual setup. You can read more about it here: Convention over configuration.

Gradle plugins provide predefined behaviors and directory structures that "just work" in most cases. Let’s take the Java Plugin as an example:

  • The default source directory is src/main/java.

  • The default output location for compiled classes and packaged artifacts (like JARs) is build/.

While Gradle allows you to override most defaults, doing so can make your build harder to understand and maintain—especially for teams or newcomers.

Stick to standard conventions unless you have a strong reason to deviate (e.g., adapting to a legacy layout). Refer to the reference documentation for each plugin to learn about its default conventions and behaviors.

Use a Settings file

Every time you run a Gradle build, Gradle attempts to locate a settings.gradle (Groovy DSL) or settings.gradle.kts (Kotlin DSL) file. To do this, it walks up the directory hierarchy from the current working directory to the filesystem root. As soon as it finds a settings file, it stops searching and uses that as the entry point for the build.

In a multi-project build, the settings file is required. It defines which projects are part of the build and enables Gradle to correctly configure and evaluate the entire project hierarchy.

You may also need a settings file to add shared libraries or plugins to the build classpath using pluginManagement or dependencyResolutionManagement.

The following example shows a standard Gradle project layout:

.
├── settings.gradle.kts
├── subproject-one
│   └── build.gradle.kts
└── subproject-two
    └── build.gradle.kts
.
├── settings.gradle
├── subproject-one
│   └── build.gradle
└── subproject-two
    └── build.gradle

Use a gradle.properties file

In Gradle, you can define properties in several ways:

  • Directly in a build script

  • In a gradle.properties file

  • On the command line using -P

The gradle.properties file provides a clean and centralized way to define reusable configuration values. It’s especially suited for build environment settings, such as:

  • JVM memory settings

  • Project-wide flags or toggles

  • API tokens (non-sensitive)

  • Feature enablement

Gradle automatically loads properties from:

  • A gradle.properties file in the project root

  • A gradle.properties file in your GRADLE_USER_HOME directory (for user-wide settings)

For example:

.
├── gradle.properties
├── settings.gradle.kts
├── subproject-a/
│   └── build.gradle.kts
└── subproject-b/
    └── build.gradle.kts
.
├── gradle.properties
├── settings.gradle
├── subproject-a/
│   └── build.gradle
└── subproject-b/
    └── build.gradle