Keywords: composite build, included build

# Composite Builds (Included Builds)

version 9.7.0

A composite build is a build that includes other builds; those builds are known as included builds.

A composite build is similar to a Gradle multi-project build, except that instead of including `subprojects`, entire `builds` are included.

This is done either using the Settings file and adding a build using `includeBuild()`:

settings.gradle.kts

    includeBuild("my-utils")

settings.gradle

    includeBuild 'my-utils'

Or using the CLI flag `--include-build`:

    $ ./gradlew build --include-build my-utils

## When to Use Composite Builds

Composite builds are useful in three broad scenarios:

1. Developing libraries and consumers together: If you maintain a library that another project depends on, composite builds let you work across both at the same time without publishing the library first.
   Gradle automatically substitutes the external dependency with a local source, so changes to the library are immediately visible in the consuming project.

2. Organizing large codebases: Composite builds let you split a large codebase into independent builds that can each be opened and worked on in isolation (for example, in the IDE), while still being buildable together as a whole.
   This is a common approach in monorepo setups.

3. Sharing build logic: Composite builds are the recommended way to structure and share custom build logic across projects.
   `buildSrc` is essentially an included build that Gradle manages automatically; a `build-logic` build included via `includeBuild()` in your settings file is the same idea, made explicit.

## Composite Build Layout

Included builds do not share any configuration with other included builds.

| Each included build is configured in isolation — included builds do not share repositories, plugins, or properties with one another or with the root build, but they can be executed together and depend on each other’s tasks.

Composite builds typically take one of two layouts depending on the use case: a co-development layout, where an included build lives outside the consumer and is substituted temporarily during development; or a monorepo layout, where an uber-root build knits together a set of independent builds that can also be worked on in isolation.

| For reference, a build tree consists of the root build and all of its included builds, recursively.

### Co-development Layout Example

The following example demonstrates co-development — temporarily substituting a dependency to work across a library and its consumer simultaneously. You simply include the library directly into the consumer build:

    my-app/                             // The consumer build (your working directory)
    ├── settings.gradle.kts             // includeBuild("../my-utils")
    ├── build.gradle.kts
    └── app/
        ├── build.gradle.kts            // Declares dependencies on 'number-utils' and 'string-utils'
        └── src/main/java/...

    my-utils/                           // The library build (Included Build)
    ├── settings.gradle.kts             // Defines 'number-utils' and 'string-utils' subprojects
    ├── number-utils/
    │   ├── build.gradle.kts
    │   └── src/main/java/...
    └── string-utils/
        ├── build.gradle.kts
        └── src/main/java/...

    my-app/                             // The consumer build (your working directory)
    ├── settings.gradle                 // includeBuild("../my-utils")
    ├── build.gradle
    └── app/
        ├── build.gradle                // Declares dependencies on 'number-utils' and 'string-utils'
        └── src/main/java/...

    my-utils/                           // The library build (Included Build)
    ├── settings.gradle                 // Defines 'number-utils' and 'string-utils' subprojects
    ├── number-utils/
    │   ├── build.gradle
    │   └── src/main/java/...
    └── string-utils/
        ├── build.gradle
        └── src/main/java/...

In this example, `my-utils` lives outside of `my-app` and is included temporarily during development. Once the library changes are published, the `includeBuild` can be removed.

The `my-app` root settings file looks as follows:

settings.gradle.kts

    rootProject.name = "my-app"

    include("app")
    includeBuild("../my-utils")

settings.gradle

    rootProject.name = "my-app"

    include("app")
    includeBuild("../my-utils")

### Monorepo Layout Example

The following example demonstrates a monorepo layout in which an uber-root build knits together independent builds.

    my-composite/                       // The root of the Composite Build
    ├── settings.gradle.kts
    ├── my-app/                         // The application build (Included Build #1)
    │   ├── settings.gradle.kts
    │   └── app/
    │       ├── build.gradle.kts        // Declares dependencies on 'number-utils' and 'string-utils'
    │       └── src/main/java/...
    └── my-utils/                       // The utility library build (Included Build #2)
        ├── settings.gradle.kts         // Defines 'number-utils' and 'string-utils' subprojects
        ├── number-utils/
        │   ├── build.gradle.kts
        │   └── src/main/java/...
        └── string-utils/
            ├── build.gradle.kts
            └── src/main/java/...

    my-composite/                       // The root of the Composite Build
    ├── settings.gradle
    ├── my-app/                         // The application build (Included Build #1)
    │   ├── settings.gradle
    │   └── app/
    │       ├── build.gradle            // Declares dependencies on 'number-utils' and 'string-utils'
    │       └── src/main/java/...
    └── my-utils/                       // The utility library build (Included Build #2)
        ├── settings.gradle             // Defines 'number-utils' and 'string-utils' subprojects
        ├── number-utils
        │   ├── build.gradle
        │   └── src/main/java/...
        └── string-utils
            ├── build.gradle
            └── src/main/java/...

In this example, `my-composite` is a composite build that includes the `my-app` build and the `my-utils` build. `my-app` and `my-utils` are included builds:

The `my-composite` root settings file looks as follows:

settings.gradle.kts

    rootProject.name = "my-composite"

    includeBuild("my-app")
    includeBuild("my-utils")

settings.gradle

    rootProject.name = 'my-composite'

    includeBuild 'my-app'
    includeBuild 'my-utils'

## Defining a Composite Build via the Settings file

The settings file, `settings.gradle(.kts)`, can be used to add subprojects and included builds simultaneously using Settings.includeBuild(java.lang.Object).

Included builds are added by location using a file path:

- Relative paths are resolved relative to the directory containing the settings file - `includeBuild("../my-utils")`

- Absolute paths can also be used but are heavily discouraged - `includeBuild("/Users/user/projects/my-utils")`

The path should point to the root directory of the build (the directory containing the `settings.gradle(.kts)` file of the included build):

settings.gradle.kts

    includeBuild("../dir/other-build")

settings.gradle

    includeBuild("../dir/other-build")

## Defining a Composite Build via `--include-build`

The `--include-build` command-line argument turns the executed build into a composite, substituting dependencies from the included build into the executed build:

    $ ./gradlew --include-build ../my-utils run

    > Task :my-plugin:pluginDescriptors
    > Task :my-plugin:processResources
    > Task :my-plugin:compileJava NO-SOURCE
    > Task :my-plugin:classes
    > Task :my-plugin:jar
    > Task :app:processResources NO-SOURCE
    > Task :my-utils:number-utils:compileJava
    > Task :my-utils:string-utils:compileJava
    > Task :my-utils:string-utils:processResources NO-SOURCE
    > Task :my-utils:string-utils:classes
    > Task :my-utils:string-utils:jar
    > Task :my-utils:number-utils:processResources NO-SOURCE
    > Task :my-utils:number-utils:classes
    > Task :my-utils:number-utils:jar
    > Task :app:compileJava
    > Task :app:classes

    > Task :app:run
    The answer is 42

    BUILD SUCCESSFUL in 0s
    10 actionable tasks: 10 executed

## Interacting with a Composite Build

Interacting with a composite build is generally similar to a regular multi-project build. Tasks can be executed, tests can be run, and builds can be imported into the IDE.

### Executing Tasks

Tasks from an included build can be executed from the command-line or IDE in the same way as tasks from a regular multi-project build. Executing a task will result in task dependencies being executed, as well as those tasks required to build dependency artifacts from other included builds.

You can call a task in an included build using a fully qualified path, for example, `:included-build-name:project-name:taskName`:

    $ ./gradlew :included-build:subproject-a:compileJava

    > Task :included-build:subproject-a:compileJava

Build, project, and task names can all be abbreviated:

    $ ./gradlew :i-b:sA:cJ

    > Task :included-build:subproject-a:compileJava

With reference to the example build, to execute the `run` task in the `my-app` build from `my-composite`:

    $ cd my-composite
    $ ./gradlew :my-app:app:run

To exclude a task from the command line, you need to provide the fully qualified path to the task:

    $ ./gradlew :my-app:app:compileJava --exclude-task :my-app:app:test

You can optionally define a `run` task in `my-composite` that depends on `my-app:app:run`:

build.gradle.kts

    tasks.register("run") {
        dependsOn(gradle.includedBuild("my-app").task(":app:run"))
    }

build.gradle

    tasks.register('run') {
        dependsOn gradle.includedBuild('my-app').task(':app:run')
    }

So that you can execute `./gradlew run` from the `my-composite` directory:

    $ cd my-composite
    $ ./gradlew run

| Included build tasks are automatically executed to generate required dependency artifacts, or the including build can declare a dependency on a task from an included build.

### Depending on Tasks

While included builds are isolated from one another and cannot declare direct dependencies, a composite build can declare task dependencies on its included builds.

The included builds are accessed using Gradle.getIncludedBuilds() or Gradle.includedBuild(java.lang.String), and a task reference is obtained via the IncludedBuild.task(java.lang.String) method.

Using these APIs, it is possible to declare a dependency on a task in a particular included build:

build.gradle.kts

    tasks.register("run") {
        dependsOn(gradle.includedBuild("my-app").task(":app:run"))
    }

build.gradle

    tasks.register('run') {
        dependsOn gradle.includedBuild('my-app').task(':app:run')
    }

Or you can declare a dependency on tasks with a certain path in all of the included builds:

build.gradle.kts

    tasks.register("publishDeps") {
        dependsOn(gradle.includedBuilds.map { it.task(":publishMavenPublicationToMavenRepository") })
    }

build.gradle

    tasks.register('publishDeps') {
        dependsOn gradle.includedBuilds*.task(':publishMavenPublicationToMavenRepository')
    }

### Referencing Projects

It is a common mistake to attempt to use `project()` notation to depend on a module that resides in an included build.

| Included builds are not subprojects.

Each build in a composite has its own project hierarchy. You cannot use project paths (e.g., `project(":other-build:module")`) to reference a project across build boundaries. Doing so will result in an `UnknownProjectException`.

To depend on a project from an included build, you must use its external coordinates (the same coordinates you would use if the library were published to Maven Central or Ivy).

If an included build has a project with `group = "com.example.data"` and `rootProject.name = "core-schema"`, you reference it in your main build like an external dependency. For example, if your configuration looks as follows:

    .
    ├── app (Composite Build)
    │   ├── build.gradle.kts
    │   └── settings.gradle.kts  <-- includeBuild("../core-schema")
    └── core-schema (Included Build)
        ├── build.gradle.kts
        └── settings.gradle.kts  <-- rootProject.name = "core-schema"

    .
    ├── app (Composite Build)
    │   ├── build.gradle
    │   └── settings.gradle  <-- includeBuild("../core-schema")
    └── core-schema (Included Build)
        ├── build.gradle
        └── settings.gradle  <-- rootProject.name = "core-schema"

The included build has the following information:

core-schema/build.gradle.kts

    group = "com.example.data"
    version = "1.0.0"

core-schema/build.gradle

    group = "com.example.data"
    version = "1.0.0"

So that the main build can reference it:

build.gradle.kts

    dependencies {
        // ❌ This will fail because ":core-schema" is not in this build's hierarchy
        implementation(project(":core-schema"))

        // ✅ Gradle sees this GAV and finds the matching project in the included build
        implementation("com.example.data:core-schema:1.0.0")
    }

build.gradle

    dependencies {
        // ❌ This will fail because ":core-schema" is not in this build's hierarchy
        implementation project(":core-schema")

        // ✅ Gradle sees this GAV and finds the matching project in the included build
        implementation "com.example.data:core-schema:1.0.0"
    }

Gradle uses the metadata from the included build to "discover" which coordinates it provides. As long as the coordinates match, Gradle replaces the external dependency with a project dependency at execution time, see Dependency Substitution in Composite Builds to learn more.

## Configuring a Composite Build

A critical aspect of included builds is that they are executed in isolation.

This has specific implications for how configuration is (or is not) shared:

- Gradle Properties: `gradle.properties` files defined in the root build are not visible to included builds. Each included build must define its own properties or properties must be passed via the CLI.
  Note that this applies to user-defined properties. Gradle runtime properties (such as `org.gradle.configuration-cache`) are a special case — they are only read from the root build and applied to the entire invocation. Any such properties defined in an included build’s `gradle.properties` are ignored.

- Shared Configuration: Included builds do not share any configuration with the root build or other included builds. This includes repositories, plugin management, or dependency versions defined in `buildSrc` or version catalogs.

Continuing from the example above, where properties are defined in the CLI and in the root `gradle.properties` file:

    $ ./gradlew checkAll -PpropertiesMessage="Hello" -DsystemMessage="Hello"

The following output is produced, showcasing the propagation of the properties from the root file and the CLI to the included builds:

    > Configure project :my-app:app
    propertiesFileMessage = null
    systemMessage = Hello
    propertiesMessage = Hello

    > Configure project :my-utils:number-utils
    propertiesFileMessage = null
    systemMessage = Hello
    propertiesMessage = Hello

    > Configure project :
    propertiesFileMessage = Hello
    systemMessage = Hello
    propertiesMessage = Hello

If you need to share credentials (like repository tokens) or common configuration across a composite build tree, consider using system environment variables or extracting the logic into a shared convention plugin included via `pluginManagement`.

While included builds are isolated and cannot declare direct project dependencies, a composite build can declare task dependencies on its included builds.

## Gradle Plugins in Composite Builds

When including builds that provide Gradle plugins, there are different approaches depending on how and where the plugin will be applied.

### Plugins for Build Scripts

Plugins that will be applied in project build scripts (e.g., `build.gradle.kts`) can be included using `includeBuild` in your `settings.gradle(.kts)` file.

This approach is recommended over using the `--include-build` CLI flag, as it provides more reliable plugin resolution, especially when using the modern `plugins {}` block:

settings.gradle.kts

    pluginManagement {
        includeBuild("my-plugin")
        repositories {
            gradlePluginPortal()
            mavenCentral()
        }
    }

settings.gradle

    pluginManagement {
        includeBuild('my-plugin')
        repositories {
            gradlePluginPortal()
            mavenCentral()
        }
    }

Then the plugin is used as follows:

my-app/app/build.gradle.kts

    plugins {
        id("application")
        id("com.example.hello") // from the Included Build in pluginManagement
    }

my-app/app/build.gradle

    plugins {
        id 'application'
        id('com.example.hello') // from the Included Build in pluginManagement
    }

### Plugins for Settings Files

A special case of included builds are builds that define plugins that need to be applied in the settings file itself. These builds should be included using the `includeBuild` statement inside the `pluginManagement {}` block of the settings file.

Using this mechanism, the included build may contribute a settings plugin that can be applied in the settings file:

settings.gradle.kts

    pluginManagement {
        includeBuild("./my-settings-plugin")
    }

    plugins {
        id("my-settings-plugin")
    }

settings.gradle

    pluginManagement {
        includeBuild('./my-settings-plugin')
    }

    plugins {
        id 'my-settings-plugin'
    }

| While the `--include-build` CLI flag can work for plugins applied via the `buildscript {}` block, it may not work reliably for plugins applied using the modern `plugins {}` block, especially for unpublished plugins.

For consistent behavior, use `includeBuild` in your settings file. For settings plugins (plugins applied in the settings file itself), use `includeBuild` within the `pluginManagement {}` block.

## Dependency Substitution in Composite Builds

By default, Gradle will configure each included build to determine the dependencies it can provide. The algorithm for doing this is simple. Gradle will inspect the group and name for the projects in the included build and substitute project dependencies for any external dependency matching `${project.group}:${project.name}`.

| By default, substitutions are not registered for the main build. To make the (sub)projects of the main build addressable by `${project.group}:${project.name}`, you can tell Gradle to treat the main build like an included build by self-including it: `includeBuild(".")`.

There are cases when the default substitutions determined by Gradle are insufficient or must be corrected for a particular composite. For these cases, explicitly declaring the substitutions for an included build is possible.

For example, a single-project build called `anonymous-library`, produces a Java utility library but does not declare a value for the group attribute:

anonymous-library/build.gradle.kts

    plugins {
        java
    }

anonymous-library/build.gradle

    plugins {
        id 'java'
    }

When this build is included in a composite, it will attempt to substitute for the dependency module `undefined:anonymous-library` (`undefined` being the default value for `project.group`, and `anonymous-library` being the root project name). Clearly, this isn’t useful in a composite build.

To use the unpublished library in a composite build, you can explicitly declare the substitutions that it provides:

settings.gradle.kts

    includeBuild("anonymous-library") {
        dependencySubstitution {
            substitute(module("org.sample:number-utils")).using(project(":"))
        }
    }

settings.gradle

    includeBuild('anonymous-library') {
        dependencySubstitution {
            substitute module('org.sample:number-utils') using project(':')
        }
    }

With this configuration, the `my-app` composite build will substitute any dependency on `org.sample:number-utils` with a dependency on the root project of `anonymous-library`.

| Substitutions are not transitive across builds.

Explicitly declared substitutions are only active for the build that defines them. In the example above, while `my-app` knows to substitute `org.sample:number-utils` with the local `anonymous-library`, the `anonymous-library` build itself remains unaware of this rule.

If you run a task directly from within the `anonymous-library` directory that requires `org.sample:number-utils` (for example, in its own test suite), the resolution will fail unless:

1. The substitution is also declared in the `settings.gradle(.kts)` of `anonymous-library`.

2. (Recommended) You align the group and `rootProject.name` of `anonymous-library` to match the expected coordinates, allowing Gradle’s default auto-substitution to work in both directions.

### Disabling Substitutions for a Configuration

If you need to resolve a published version of a module that is also available as part of an included build, you can deactivate the included build substitution rules on the `ResolutionStrategy` of the Configuration that is resolved. This is necessary because the rules are globally applied in the build, and Gradle does not consider published versions during resolution by default.

For example, we create a separate `publishedRuntimeClasspath` configuration that gets resolved to the published versions of modules that also exist in one of the local builds. This is done by deactivating global dependency substitution rules:

build.gradle.kts

    configurations.create("publishedRuntimeClasspath") {
        resolutionStrategy.useGlobalDependencySubstitutionRules = false

        extendsFrom(configurations.runtimeClasspath.get())
        isCanBeConsumed = false
        attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
    }

build.gradle

    configurations.create('publishedRuntimeClasspath') {
        resolutionStrategy.useGlobalDependencySubstitutionRules = false

        extendsFrom(configurations.runtimeClasspath)
        canBeConsumed = false
        attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, Usage.JAVA_RUNTIME))
    }

A use-case would be to compare published and locally built JAR files.

### When to Declare Substitutions Explicitly

Many builds will function automatically as an included build, without declared substitutions. Here are some common cases where declared substitutions are required:

- When the `archivesBaseName` property is used to set the name of the published artifact.

- When a configuration other than `default` is published.

- When the `maven-publish` or `ivy-publish` plugins are used for publishing and the publication coordinates don’t match `${project.group}:${project.name}`.

- When multiple publications are defined for a single project (e.g., a project that publishes two different libraries).

### When Substitutions Won’t Work

Some builds won’t function correctly when included in a composite, even when dependency substitutions are explicitly declared. This limitation is because a substituted project dependency will always point to the `default` configuration of the target project. Any time the artifacts and dependencies specified for the default configuration of a project don’t match what is published to a repository, the composite build may exhibit different behavior.

Here are some cases where the published module metadata may be different from the project default configuration:

- When the `maven-publish` or `ivy-publish` plugins are used with custom logic in the publication block.

- When the `POM` or `ivy.xml` file is tweaked as part of publication.

- When a capability or a custom variant is only defined via publication metadata.

Builds using these features may function incorrectly when included in a composite build.

## Requirements for Composite Builds

Composite builds must maintain unique identifiers for every project across the entire build tree.

To prevent conflicts, Gradle qualifies projects using a build-tree path, which combines the build path (derived from the included build’s directory name) and the project path.

To ensure every project remains uniquely addressable, included builds must satisfy two conditions:

1. Unique Build Paths: No two included builds can share the same build path.

2. No Namespace Overlap: An included build path cannot conflict with any existing project path in the root build.

If directory names cause a naming collision, rename the build path in your settings file:

settings.gradle.kts

    includeBuild("some-included-build") {
        name = "other-name"
    }

settings.gradle

    includeBuild('some-included-build') {
        name = 'other-name'
    }

| When a composite build includes another composite, Gradle flattens the structure. All included builds share the same parent, regardless of how deeply they were nested.

## Known Limitations of Composite Builds

Limitations of the current implementation include:

- Substitution Limitations: No support for included builds with publications that don’t mirror the project default configuration. See Cases where composite builds won’t work.

- Multiple Composite Builds: Multiple composite builds may conflict when run in parallel if more than one includes the same build. Gradle does not share the project lock of a shared composite build between Gradle invocations to prevent concurrent execution.

- Configuration-on-Demand: See Configuration-on-Demand below.

### Configuration-on-Demand

Using Configuration-on-Demand with composite builds can significantly impact performance. The behavior changes depending on whether you rely on default substitutions or explicitly declared substitutions:

1. You must manually declare all dependency substitutions for every project within that included build that you wish to use. Any project not explicitly mapped will be treated as an external dependency.

2. The way Gradle discovers what an included build "provides" dictates how much of that build must be configured at startup:

   Scenario | Behavior | Performance Impact
   --- | --- | ---
   Default Rules (No explicit substitutions) | Gradle must inspect every project in the included build to find matching `group` and `name`. | High: The included build is always fully configured, even if its projects aren’t needed for the current task.
   Explicit Rules (Using `substitute…​`) | Gradle uses your explicit rules as the source of truth. | Optimized: The included build is not configured unless a task requires one of the substituted projects.