Isolated Projects
Isolated Projects is an incubating feature aimed at improving Gradle scalability and performance, especially for large builds and IDE sync (Android Studio / IntelliJ IDEA).
When the Isolated Projects feature is enabled, the projects in a multi-project build are "isolated" from each other. This means that build logic, such as build scripts or plugins, applied to a project cannot directly access the mutable state of other projects. This constraint allows safely running project configuration in parallel, improving performance. Further optimizations, such as finer-grained caching of configuration results, are possible but are not yet implemented.
You can enable the feature in the properties file:
org.gradle.isolated-projects=true
Alternatively, the flag can be passed on the command line:
$ ./gradlew build --isolated-projects
Follow the recommended migration steps to make your build logic compatible with Isolated Projects constraints.
|
Isolated Projects is an incubating feature. APIs and behavior may still change in future Gradle versions while it remains incubating. Such changes are highlighted in the release notes for each release. |
Recent changes
-
Gradle 9.7.0
-
Isolated Projects is promoted from experimental to incubating.
-
The stable
org.gradle.isolated-projectsproperty names are now preferred, and the earlier.unsafenames are deprecated. -
The build now fails immediately on the first violation by default. Use the Diagnostics mode to discover as many violations as possible in a single run.
-
Many
GradleAPIs are now treated as incompatible when accessed during project configuration, protecting the mutable build-scoped state. -
The new temporarily ignore violations mode reports violations without failing the build, so you can evaluate performance before fixing them.
-
The Configuration Cache
--configuration-cache-problems=warnflag no longer downgrades Isolated Projects violations to warnings. -
The
-x/--exclude-tasksoption is now supported on the command line.
-
-
Gradle 9.6.0
-
Additional APIs on
BuildServiceRegistryare treated as incompatible:getRegistrationsnow only permits thefindByName(String)method. All other methods emit Isolated Projects violations.registerIfAbsentremains Isolated Projects compatible. -
Properties and methods are no longer looked up in parent projects, avoiding false-positive violations and reducing project coupling.
-
The
Project.getProperties()API is now treated as incompatible, while being generally deprecated. -
The new Diagnostics mode runs without parallelism and deterministically reports constraint violations.
-
Editor support for Kotlin build scripts and precompiled plugins in IDEA-based IDEs has been fixed.
-
The minimum supported version of the Develocity plugin is 4.0.
-
-
Gradle 9.4.0
-
Isolated Projects violations are now deduplicated, making the Configuration Cache HTML reports significantly smaller.
-
-
Gradle 9.3.0
-
Tooling model caching is now disabled, as it currently has known correctness problems.
-
-
Gradle 9.2.0
-
Additional APIs on
TaskContainerare treated as incompatible:findByPath(String)andgetByPath(String)now emit violations when a task from another project is requested.
-
Performance benefits and limitations
The main goal of Isolated Projects is to speed up or entirely avoid running the configuration phase. This is similar to the Configuration Cache feature, which allows skipping configuration when you rerun a task without changing the build configuration inputs. In fact, Isolated Projects uses Configuration Cache infrastructure as the foundation.
Builds that will get the most benefit are those that have a long configuration phase, such as large multi-project builds. However, even for small builds, keeping the configuration time under a few seconds helps developers stay in flow and avoid losing momentum.
Here is how Isolated Projects affects the lifecycle of the build:
-
Initialization phase
Running init scripts and evaluating settings remains sequential with Isolated Projects. Compared to other phases, this phase is already relatively quick.
-
Configuration phase
Isolated Projects directly improves the performance of this phase by running project configuration in parallel.
-
Execution phase
Isolated Projects has no effect on this phase. Tasks are already isolated from each other and run in parallel with Configuration Cache, which is a prerequisite for Isolated Projects.
|
In case of a Configuration Cache hit, the initialization and configuration are skipped entirely. This applies with Isolated Projects as well. |
An important takeaway is that enabling Isolated Projects on a machine with N cores will not make your build N times faster. If the configuration phase is only 40% of the build time, then that’s the ceiling for improvement with Isolated Projects. That said, for large builds, even a few-percent improvement can translate into minutes or more of actual time, which can have a substantial impact on developer productivity.
Overall, the performance benefits of Isolated Projects depend on a variety of factors, such as the size and shape of your build, hardware resources, as well as your workflow. It also depends on the way Gradle is invoked.
Gradle invocation modes
Most users interact with Gradle in different ways:
-
Running tasks (either via command-line or via IDE)
-
Building tooling models (this happens behind the scenes when you do an IDE sync)
The configuration phase runs in both cases, but it is significantly different when model building is involved. Because of this, the performance benefits of Isolated Projects also depend on the way of interacting.
|
Performance improvements for Isolated Projects are released gradually across Gradle versions. Some improvements might bring benefits only for running tasks, while others for model building (and IDE sync by extension). As the Isolated Projects feature matures, the improvements will serve more scenarios. |
Performance when running tasks
When running tasks, the configuration phase consists of multiple steps.
-
Configure projects
Isolated Projects makes this step run in parallel, which can significantly improve performance for builds with many projects. In the current implementation, all projects are always configured; there is no project configuration avoidance yet, whether by skipping unneeded projects or by using cached results.
-
Discover the work graph
The work graph is constructed by starting with the requested tasks and following the dependencies until all required work is discovered. This is not a directly parallelizable process. In the current implementation, even with Isolated Projects, this step is sequential. For invocations that result in having large work graphs, this step can become a performance bottleneck. An example is running checks or tests on all projects via an unqualified task, such as
./gradlew check, which is typical on CI. -
Store the work graph (Configuration Cache)
Isolated Projects makes this step run in parallel. This is the same as with Parallel Configuration Cache, but with the additional safety provided by the Isolated Projects constraints.
-
Load the work graph (Configuration Cache)
This step is already parallel with Configuration Cache; Isolated Projects has no additional effect here.
Performance when building models (IDE sync)
For scenarios such as IDE sync, Gradle provides a special mechanism of building tooling models. The models describe the structure of the build, tell IDEs where to find production sources, and more.
From Gradle’s point of view, an IDE sync or a tooling invocation in general consists of the following parts (simplified):
-
Configure projects
Isolated Projects makes this step run in parallel, which can significantly improve performance for builds with many projects. In the current implementation, all projects are always configured; there is no project configuration avoidance yet, whether by skipping unneeded projects or by using cached results.
-
Run model builders
The invoking tool (e.g. the IDE) controls how individual models are built. Because of this, enabling Isolated Projects alone might not be enough to build models in parallel. However, in some cases it’s possible to achieve parallel model building even without Isolated Projects, as explained below.
-
Return the full model
Similarly to how the task-running invocation finishes after all tasks have executed, the model-building invocation finishes after returning the final model. The model content is fully defined by the tool, e.g. the IDE.
|
There is a lot more nuance to this, but it’s out of scope of this page. Individual models can be streamed back to the consumer before the final return. A tooling invocation can also request running tasks and build models before and after. All this and more affect the performance of a model building invocation and how Isolated Projects can improve it. |
Without Isolated Projects, it is not possible to get a "Configuration Cache hit" for IDE sync because Configuration Cache does not currently support model building invocations. When Isolated Projects is enabled, the full model can be cached and returned immediately on later invocations if the build configuration hasn’t changed.
Parallel model building for IDE sync
Parallel model building can significantly speed up IDE sync. However, both Gradle and the IDE need to be configured to allow it.
With Isolated Projects enabled, parallel model building happens automatically in the following IDE versions:
-
Android Studio Electric Eel (2022.1.1) or later
-
IntelliJ IDEA 2026.1.1 or later
In earlier IntelliJ IDEA versions, it has to be manually enabled in
Settings › 'Build, Execution, Deployment' › Build Tools › Gradlevia theEnable parallel Gradle model fetchingcheckbox.
If you have already been using parallel model building for IDE sync, then Isolated Projects will not provide a performance benefit in that part of the configuration phase. However, Isolated Projects will catch violations as you change your build logic, preventing correctness issues from going unnoticed.
|
Enabling parallel model building without Isolated Projects can be unsafe. Parts of the configuration logic will run in parallel without supporting validations, potentially leading to flakiness or unreliable results. Isolated Projects makes this safe by enforcing additional constraints. |
Parallelism controls
Much of the performance benefit of Isolated Projects comes from running project configuration in parallel.
The degree of parallelism is controlled by the org.gradle.workers.max Gradle property,
which sets the maximum number of parallel units of work across all build phases,
including configuration.
The default value is the number of CPU cores on your machine, which is usually a good choice. If you’ve explicitly set it lower for any reason, keep in mind that it will also limit how much parallelism Isolated Projects can use.
Current limitations
The current implementation of Isolated Projects has a number of limitations that might be addressed in the future.
-
All projects always get configured, and Configuration on Demand has no effect.
-
A subproject can’t start configuring until all its parent projects are done, which limits the parallelism.
-
The configuration phase might require more memory to process more work in parallel.
-
Changes to included builds invalidate all cached results, even unrelated ones.
-
Tooling model caching is local only: no sharing across user-home caches or remote caching.
-
A
gradle.lifecycle.beforeProjectcallback registered insettings.gradle(.kts)cannot directly apply a lazily-resolved plugin, such as a convention plugin from a build registered viapluginManagement.includeBuild(…)or a third-party plugin from a repository, because the callback alone does not trigger resolution of that plugin and the export of its classpath. See Applying plugins from a lifecycle callback for working patterns. -
Isolated Projects violations are reported as Configuration Cache problems.
Evaluating Isolated Projects for your build
The benefit Isolated Projects brings depends on the size and shape of your build, your hardware, and your workflow (see performance benefits and limitations). Rather than predicting the gain from the structure of your build, the most reliable approach is to measure it directly:
-
Enable the feature
Make the build compatible with Configuration Cache first, then enable Isolated Projects.
-
Get an early estimate before completing the migration
You don’t have to fix every violation before measuring. Use the dangerously ignore problems mode to gauge the speed-up for your actual workflows — both running tasks and IDE sync — while violations are still present.
-
Compare configuration-phase times
Measure the configuration phase with and without the feature using a Build Scan or profile report. Keep in mind that Isolated Projects can only speed up the configuration phase, so it can only improve the build time that phase represents: the potential gain is bounded by how much of the total build time it takes up. The execution phase duration is unaffected.
Isolated Projects constraints
During configuration, Gradle runs lots of smaller units of work (evaluating settings, configuring individual projects, etc.),
and each one mutates live state on objects like Project, Settings, Gradle.
Isolated Projects doesn’t change how individual projects or included builds are configured; plugins still mutate the state of the target they’re applied to. What it adds is strict boundaries that prevent projects and builds from observing or mutating each other’s state.
With Isolated Projects, it is not allowed to observe or change:
-
In project-scope, mutable state of other projects (cross-project access)
-
In project-scope, mutable state of the owner build (project-to-build access)
-
In build-scope, mutable state of other builds (cross-build access)
-
In build-scope, mutable state of the build that is passed into project-scope via callbacks (build-to-project access)
Among the build logic patterns described by these constraints, cross-project access is by far the most common.
The constraints are designed to allow safely running the individual configuration units in parallel and also caching the results of their execution.
Constraint violations
The violations of Isolated Projects constraints are detected at runtime during an invocation. When at least one violation is detected, the build results cannot be considered reliable and the build will fail.
Here is an example of the build logic that contains a violation:
// ⚠️ `Project.version` is mutable state of the root project and cannot be accessed from a subproject
val commonVersion = rootProject.version
// ⚠️ `Project.version` is mutable state of the root project and cannot be accessed from a subproject
def commonVersion = rootProject.version
$ ./gradlew --isolated-projects
...
- Build file 'app/build.gradle.kts': Project ':app' cannot access 'Project.version' functionality on another project ':'
...
BUILD FAILED in 0s
Cross-project access constraints
The cross-project access constraints apply during project configuration when a Project instance representing another project is involved.
Each instance of Project has both mutable and immutable state.
Accessing mutable state will result in a violation, but accessing immutable state is allowed.
Restricted access to mutable data of other projects
Most parts of the Project state are directly or indirectly mutable via the methods on the interface.
Accessing that state on another project is not allowed with Isolated Projects.
- Examples of mutable state (not exhaustive)
-
-
Basic state:
description,group,version,status,buildDir -
Containers:
tasks,dependencies,configurations,artifacts,repositories,components,plugins,extensions,ext -
Services:
layout,providers,objects,dependencyFactory
-
The Project interface also provides many convenience methods that allow configuring it, or registering callbacks.
Most of those methods indirectly access the mutable state and are also not allowed to be called on another project.
In general, it is easier to assume calling any method on another Project is forbidden,
unless it is one of the handful getters that provide immutable data.
Unrestricted access to immutable data of other projects
Immutable project state includes data that was configured during settings evaluation and finalized before any project configuration has begun. This includes basic information about the project, but also the structure of the project hierarchy, since it can no longer be changed during project configuration.
- Immutable project data
-
-
Basics:
name,displayName,path,buildTreePath,depth-
Caution:
groupandversionare mutable!
-
-
Directories/files:
projectDir,buildFile,rootDir-
Caution:
buildDirandlayoutare mutable!
-
-
- Project hierarchy navigation
-
-
isolated: IsolatedProject -
rootProject: Project -
parent: Project? -
project(): Projectoverloads -
childProjects: Map<String, Project> -
subprojects()andallprojects()overloads
-
It is allowed to traverse the project structure and access immutable data of other projects. However, it is best to reduce such interactions to a minimum and instead express them through dependencies between projects.
Project-to-build access constraints
Project configuration logic has access to the build-scoped state shared across all projects via the
Gradle object, reachable through project.gradle.
Each Gradle instance exposes both immutable build information and mutable build-scoped state.
Accessing the mutable state from a project results in a violation, while accessing immutable build information is allowed.
A large part of the Gradle interface is dedicated to callback registration.
Registering these callbacks from a project couples that project to build-scope state and to the lifecycle of other projects, which is not safe when projects are configured in parallel.
Callbacks that do get successfully registered may also execute in a different order per invocation.
Restricted access to mutable build state
Registering listeners or callbacks, applying plugins, or mutating extensions on the Gradle object from a project is not allowed with Isolated Projects, and produces a violation like:
- Build file 'app/build.gradle.kts': Project ':app' cannot access 'Gradle.extensions'
- Examples of restricted members (not exhaustive)
-
-
Listeners:
addListener,removeListener,addBuildListener,useLogger -
Project-evaluation callbacks:
addProjectEvaluationListener,removeProjectEvaluationListener,beforeProject,afterProject,lifecycle -
Plugins:
apply,plugins,pluginManager -
Extensions:
extensions
-
|
The |
|
|
Unrestricted access to immutable build information
Immutable build information is finalized before project configuration begins and is safe to read from any project.
- Immutable build data
-
-
Versions and directories:
gradleVersion,gradleUserHomeDir,gradleHomeDir -
Build identity and structure:
buildPath,includedBuilds,includedBuild(name) -
Services:
providers,startParameter
-
Shared Build Services
Shared Build Services are also part of the build-scoped state. As the name suggests, they are shared between projects in the build. However, the API for registering and observing their registrations was not designed with parallel project configuration in mind.
Generally speaking, registering a build service via gradle.sharedServices.registerIfAbsent(…) is safe with Isolated Projects.
Only the first registration succeeds, and any later registrations return the previously registered instance.
However, the parameters of the build service can become shared mutable state.
Any mutation of the build service parameters outside of registerIfAbsent(…) { parameters { … } } should be avoided.
Accessing the set of build service registrations at gradle.sharedServices.registrations is restricted with Isolated Projects.
Only findByName(String) is permitted on the returned set; all other methods emit Isolated Projects violations.
Use registerIfAbsent(String, Class) to obtain a service provider instead of querying the registrations set directly.
Cross-build access constraints
|
Cross-build access constraints are not fully enforced yet. |
Build configuration logic (init scripts and settings) has access to the state of other builds via gradle.parent.
Since (included) builds can be configured in parallel, access to the shared mutable state is also problematic.
With Isolated Projects, using gradle.parent should be avoided.
Build-to-project access constraints
|
Build-to-project access constraints are not fully enforced yet. |
Build configuration logic (init script and settings) can affect project configuration by registering callbacks that will run later in the build lifecycle.
Some of the callbacks are executed around project evaluation time: immediately before or after the project configuration. With Isolated Projects, when project configuration runs in parallel, these callbacks might execute in parallel as well. If the callbacks capture any mutable state from the build-scope, that state becomes shared mutable state, leading to concurrency problems.
See the section on state-isolating callbacks for a better alternative.
Behavior changes
When running builds with and without Isolated Projects you might observe differences in the behavior of Gradle APIs and DSLs. This is especially relevant while you migrate your build, when Isolated Projects is not yet enabled for all environments or workflows.
The presence of behavior changes is an exception rather than the rule. The Isolated Projects constraints validate the usages of APIs to prevent cases where intermediate results or the build outcome could have been different.
No implicit lookup of properties and methods in parent projects
Under Isolated Projects, the implicit resolution of properties and methods from parent projects does not happen. In Gradle 9.6.0, this behavior was deprecated when running without Isolated Projects. The corresponding upgrade guide entry details the cases when it is applicable.
|
The |
The lenient property lookups are common place in build logic and plugins. Due to this, it is highly recommended to address relevant deprecations before migrating to Isolated Projects.
You can align the behavior across all Gradle modes by using a feature preview flag. It is recommended to enable this flag in builds that are migrating to or already using Isolated Projects.
enableFeaturePreview("NO_IMPLICIT_LOOKUP_IN_PARENT_PROJECTS")
Properties report does not include properties inherited from parent projects
Under Isolated Projects, the properties report of a child project lists only the properties defined on that project itself. Properties inherited from parent projects are not present in the report. This is a consequence of the no implicit parent lookup behavior.
Direct uses of Project.getProperties()
continue to be reported as an Isolated Projects violation, as that API is being phased out.
Migration
This section covers how to migrate your build to be compatible with Isolated Projects.
|
Isolated Projects is an incubating feature, and it is actively changing. Make sure to use the latest available version of Gradle and that you are reading the documentation for that version. |
Before the migration
Before addressing constraint violations, make sure your build setup is up to date to get the best adoption experience.
-
Upgrade the build to the latest version of Gradle
If available, consider a pre-release version such as a milestone or a nightly to get the best experience while Isolated Projects is incubating.
-
Update third-party plugins to their latest versions
Ecosystem plugins and community plugins gradually improve support for Isolated Projects. By using the latest versions you reduce the risks of dealing with problems that have already been fixed.
-
Use the latest tooling, such as the latest versions of Android Studio and IntelliJ IDEA
Newer IDE versions can bring better performance as well as a better user experience when adopting Isolated Projects. It is recommended to use IntelliJ IDEA 2026.1.1 or later.
-
Make your build compatible with Configuration Cache
The Isolated Projects feature builds on top of the Configuration Cache feature and requires the build to be compatible with it.
-
Minimize behavior changes by addressing relevant deprecations
Follow the guidance for each behavior change to ensure expected build results when gradually migrating workflows to run with Isolated Projects.
Enabling Isolated Projects
Isolated Projects can be enabled by setting the org.gradle.isolated-projects Gradle option to true, or by passing the --isolated-projects flag on the command line:
$ ./gradlew help --isolated-projects
To make use of the feature in IDE sync scenarios, it has to be enabled in the gradle.properties file:
org.gradle.isolated-projects=true
|
The legacy property name |
If you have never enabled the feature in the build before, it’s likely that the build is not compatible and contains Isolated Projects constraint violations. In that case the build fails immediately and reports one or more violations in the error message. To discover as many violations as possible in a single run, use the Diagnostics mode described below.
Diagnostics mode
Diagnostics mode is an opt-in mode that helps you discover Isolated Projects constraint violations more efficiently during migration. It runs configuration of projects sequentially, which allows the build to safely continue even after an Isolated Projects violation is detected.
Enable Diagnostics mode by setting org.gradle.isolated-projects.diagnostics=true in gradle.properties, alongside enabling Isolated Projects:
org.gradle.isolated-projects=true
org.gradle.isolated-projects.diagnostics=true
Alternatively, the flag can be passed on the command line:
$ ./gradlew help \
--isolated-projects \
-Dorg.gradle.isolated-projects.diagnostics=true
Compared to default Isolated Projects behavior, Diagnostics mode disables several optimizations to maximize the completeness and consistency of the violation reporting:
-
Project configuration runs sequentially instead of in parallel.
-
Configuration Cache store runs sequentially.
-
Per-project model reuse across builds is disabled, so every model is rebuilt after an input change.
-
All projects are configured, ignoring any Configuration on Demand setting.
This mode is intended to be used during the migration or to compare against the default Isolated Projects behavior when investigating a discrepancy. Because parallelism and caching are deliberately disabled, builds running in this mode will be slower. For these reasons, we generally don’t advise enabling this mode persistently or committing it to version control.
|
Configuration Cache reuse is not affected by this mode. A cache entry is stored and can be reused only when the invocation is violation-free. To re-exercise configuration, change a build input as you would normally. |
Diagnostics mode does not change which patterns count as violations. A build that is clean under Isolated Projects is also clean under Diagnostics mode.
Also, if a diagnostic run is free of violation, it doesn’t mean that the entire build is guaranteed to be fully compatible with Isolated Projects. While the configuration of all projects is ensured, violations can hide behind additional layers of laziness, such as lazy configuration of tasks.
Temporarily ignoring violations
|
The mode described in this section is a migration and troubleshooting aid and not intended as a persistent way of ignoring incompatibilities. Because the violations are ignored rather than fixed, build outputs may be incorrect, so the results should not be used to produce artifacts. It will also not prevent new incompatibilities being accidentally added to your build later. |
To guarantee reliable build results, Gradle will fail the build when any violations of Isolated Projects constraints are detected. However, in the initial stages of the Isolated Projects adoption journey, it can be useful to gauge an estimate of the speed-up provided by the feature in your particular setup and workflow, before all the violations have been fixed. It is especially relevant for the IDE sync scenarios, where the violations otherwise prevent a sync from completing.
For this purpose, Isolated Projects offers a dangerously ignore problems mode that still reports violations but does not fail the build on the basis of their presence.
Enable it by setting org.gradle.isolated-projects.dangerously-ignore-problems=true in gradle.properties, alongside enabling Isolated Projects:
org.gradle.isolated-projects=true
# ⚠️ Temporary measure of ignoring Isolated Projects violations. Do not enable permanently!
org.gradle.isolated-projects.dangerously-ignore-problems=true
Alternatively, the flag can be passed on the command line:
$ ./gradlew help \
--isolated-projects \
-Dorg.gradle.isolated-projects.dangerously-ignore-problems=true
When the mode is active, Gradle prints a banner at the start and end of the build to remind you that violations are being ignored.
|
This does not guarantee that the build will succeed. Concurrency issues may still cause exceptions which fail the build. This mode can be combined with Diagnostics mode to prevent concurrency errors, which may be helpful for an IDE sync that would otherwise be interrupted by such a failure. |
Recommended migration path
This section describes how to start making your build compatible with Isolated Projects. If you haven’t already, complete the before the migration steps first to get the best adoption experience.
-
Learn about incompatibilities in basic workflows
Run the
helptask with Isolated Projects in the Diagnostics mode:$ ./gradlew help --isolated-projects -Dorg.gradle.isolated-projects.diagnostics=trueThis way Gradle can detect Isolated Projects violations that are present in virtually any workflow and should be addressed first.
You can find the Isolated Projects violations in the Configuration Cache HTML report. Use the report to pinpoint the incompatible pieces of build logic and plugins.
-
Report the violations coming from the community plugins to their maintainers
An incompatible plugin can prevent you from getting the performance benefits of Isolated Projects. It is best to report the incompatibilities as soon as possible so the plugin maintainers can address them, while you make your own build logic compatible.
-
Follow the strategies for making build logic compatible to refactor the incompatible parts of your build
Remember that Isolated Projects violations only detect concrete instances of the problem, e.g. one specific project touching some mutable state of another. It might not be enough to replace one API call with another to address the problem.
-
Migrate the IDE sync workflow
For IDE sync, the feature flag and Diagnostics mode must be enabled in the
gradle.propertiesfile to take effect.gradle.propertiesorg.gradle.isolated-projects=true org.gradle.isolated-projects.diagnostics=trueThe IDE sync might exercise other code paths in the build logic or plugins, surfacing new violations that need to be addressed.
Remember to disable Diagnostics mode once the violations are addressed to ensure you are getting the performance improvements.
-
Migrate other workflows
Use the same approach for other scenarios, such as sanity checks and tests.
You may observe different violations depending on which tasks you run. This is expected, since incompatible build logic can hide behind lazy task configuration.
Making build logic compatible
This section collects the strategies for resolving Isolated Projects violations in your own build logic. They are ordered by preference: prefer refactoring cross-project configuration into convention plugins or state-isolating callbacks, and reach for guarding only as a last resort.
Use convention plugins
Convention plugins package shared build logic so that it is defined once and then explicitly applied in each project that needs it. This is already the Gradle best practice for sharing build logic, independent of Isolated Projects.
That same organizing principle also resolves one of the most common sources of Isolated Projects violations: projects trying to configure other projects.
This is often done to have a single place where some configuration logic is defined and then reused by applying it to many projects via callbacks like allprojects {}.
Convention plugins address the same need from a different perspective: because each project applies the plugins to itself, no Isolated Projects constraints are violated.
Additionally, convention plugins can be composed. This allows for more elaborate reuse of build logic without compromising Isolated Projects compatibility.
Use state-isolating lifecycle callbacks
Convention plugins cover most cases, but sometimes it is still desirable to apply configuration to many projects at once, from a single place.
This is usually done with a callback such as allprojects {} called from a project or registered in the build scope via gradle.
One problem with these callbacks is that the owner of the callback ends up touching state of another scope. However, there is another subtle yet important issue: callbacks can inadvertently share mutable state of the owner with all targets of the callback. When the target projects are configured in parallel, it’s not possible to safely run the corresponding callbacks.
The solution Gradle offers is state-isolating callbacks. This works on the same principles as the isolation of task state performed by Configuration Cache to allow running tasks in parallel.
The GradleLifecycle API,
accessible via gradle.lifecycle, offers beforeProject and afterProject callbacks.
Actions registered as GradleLifecycle callbacks are isolated and will run in an isolated context that is private to each project.
The example below shows how this API could be used in a settings script or settings plugins to apply configuration to all projects, while avoiding cross-project configuration:
include("sub1")
include("sub2")
gradle.lifecycle.beforeProject {
apply(plugin = "base")
repositories {
mavenCentral()
}
group = "org.example.app"
version = providers.fileContents(
isolated.rootProject.projectDirectory.file("version.txt")
).asText.get()
}
include('sub1')
include('sub2')
gradle.lifecycle.beforeProject {
apply plugin: 'base'
repositories {
mavenCentral()
}
group = 'org.example.app'
version = providers.fileContents(
isolated.rootProject.projectDirectory.file('version.txt')
).asText.get()
}
|
The example above applies the |
Applying plugins from a lifecycle callback
A common starting point is a plugin applied to every project from an allprojects {} (or subprojects {}) block in the root build script.
Under Isolated Projects this is not allowed, because it configures projects other than the one owning the script.
A third-party plugin used this way is usually declared with apply false in the root build script:
plugins {
id("com.example.foo") version "1.2.3" apply false
}
allprojects {
apply(plugin = "com.example.foo") // Not Isolated Projects-compatible
}
plugins {
id 'com.example.foo' version '1.2.3' apply false
}
allprojects {
apply plugin: 'com.example.foo' // Not Isolated Projects-compatible
}
Replacing the allprojects {} block with a gradle.lifecycle.beforeProject callback is the natural next step, but it is not enough on its own.
A gradle.lifecycle.beforeProject (or afterProject) callback can only apply a plugin that is already on the project’s plugin classpath.
The callback merely registers an action; it does not resolve plugins itself.
So apply(plugin = "…") from a callback fails with Plugin with id '…' not found for any plugin that is resolved lazily (only built or downloaded when a plugins {} block requests it), unless it has already been resolved onto a classloader scope the project inherits.
This affects two common cases:
-
a third-party plugin from a repository, such as the Gradle Plugin Portal or a Maven repository
-
a convention plugin from a build registered via
pluginManagement.includeBuild(…)
Using such a plugin from a plugins {} block in settings.gradle(.kts) or any project script works, because the block itself triggers resolution.
Using it from a lifecycle callback does not work, because nothing has triggered resolution by the time the callback runs at project configuration.
The callback runs against a classloader scope that does not see the root build script’s classpath, so the plugin declaration must move into settings.gradle(.kts) as well.
This limitation does not apply to plugins defined in buildSrc, which is built eagerly and whose classpath is exported to every project, so a gradle.lifecycle.beforeProject callback can apply a buildSrc convention plugin directly, with none of the workarounds below.
Moving build logic into buildSrc solely to sidestep this limitation is not recommended, however; Gradle best practice is to favor build-logic composite builds over buildSrc.
Two workarounds are available:
-
Register
beforeProjectfrom a settings convention plugin — move the callback registration into a settings convention plugin, where the included build’s classpath is already available. This is the recommended approach. -
Pre-resolve in the settings
plugins {}block — declare the plugin in the settingsplugins {}block withapply falseto force resolution and export its classpath to every project.
The following sections describe each workaround in detail.
Register beforeProject from a settings convention plugin (recommended)
Publish two precompiled script plugins from your included build (named build-logic here): the project convention plugin you want to apply, and a settings convention plugin that registers the gradle.lifecycle.beforeProject callback which applies it.
Both live in the included build, for example under build-logic/src/main/groovy:
plugins.apply('base')
gradle.lifecycle.beforeProject {
it.apply plugin: 'my.convention'
}
Inside the settings convention plugin the included build’s classpath is naturally available, and the project convention plugin it applies is visible to all projects via the standard plugin-resolution flow.
Apply the settings convention plugin from settings.gradle(.kts):
pluginManagement {
includeBuild("build-logic")
}
plugins {
id("my.lifecycle")
}
include("sub1")
include("sub2")
pluginManagement {
includeBuild('build-logic')
}
plugins {
id('my.lifecycle')
}
include('sub1')
include('sub2')
Pre-resolve in the settings plugins {} block
Declare the plugin in the settings plugins {} block with apply false.
This forces resolution at settings evaluation time and exports the classpath into the settings classloader scope — the parent of every project’s scope — without applying the plugin there, so the beforeProject callback can find it.
This works for both cases above.
For a convention plugin from an included build:
pluginManagement {
includeBuild("build-logic")
}
plugins {
// Resolves `my.convention` against the included plugin build and
// exports its classpath to all projects, without applying it here.
id("my.convention") apply false
}
include("sub1")
include("sub2")
gradle.lifecycle.beforeProject {
apply(plugin = "my.convention")
}
pluginManagement {
includeBuild('build-logic')
}
plugins {
// Resolves `my.convention` against the included plugin build and
// exports its classpath to all projects, without applying it here.
id('my.convention') apply false
}
include('sub1')
include('sub2')
gradle.lifecycle.beforeProject {
apply plugin: 'my.convention'
}
For a third-party plugin from a repository, include the plugin version so it can be resolved (or declare the version in pluginManagement { plugins { … } } and request it without a version here):
plugins {
id("com.example.foo") version "1.2.3" apply false
}
gradle.lifecycle.beforeProject {
apply(plugin = "com.example.foo")
}
plugins {
id 'com.example.foo' version '1.2.3' apply false
}
gradle.lifecycle.beforeProject {
it.apply plugin: 'com.example.foo'
}
|
These patterns are workarounds for the current implementation limitation listed under Current limitations. |
Use IsolatedProject instead of Project
The previous strategies address configuring other projects; this one addresses reading their state.
When build logic needs some data from another project, prefer the isolated view over the full Project, so that only safe cross-project data is reachable.
You can get a simplified view of a project by calling project.isolated,
which returns IsolatedProject type.
This works in all contexts and is especially useful when accessing an instance of another project,
such as project(":foo").isolated or rootProject.isolated.
The data provided by IsolatedProject is limited and exposes only properties that are safe to access across project boundaries.
The use of the isolated view makes it obvious to build logic readers that no cross-project state access is possible.
The example below shows how the API could be used from a Project configuration callback to query the root project directory:
gradle.lifecycle.beforeProject {
val rootDir = project.isolated.rootProject.projectDirectory
println("The root project directory is $rootDir")
}
gradle.lifecycle.beforeProject {
def rootDir = project.isolated.rootProject.projectDirectory
println("The root project directory is $rootDir")
}
Guard incompatible build logic as a last resort
When the strategies above cannot be applied, guarding is the fallback. During adoption, some build logic may not yet be compatible with Isolated Projects and cannot be fixed immediately. For example, a plugin that is still being migrated, or a third-party plugin that is not essential to every workflow. As a last resort, you can guard such logic so that it only runs when Isolated Projects is not active.
|
Guarding with a conditional is not advised if it is possible to fix the incompatibility instead. Prefer the refactoring strategies above. A conditional makes the build behave differently depending on whether Isolated Projects is active, which raises complexity, can hide problems, and can lead to different build results between the two modes. |
To detect whether Isolated Projects is active, inject the BuildFeatures service and read isolatedProjects.active.
See Reacting to build features for details on this API.
Guard logic inside your own plugin
Guard the incompatible part of a plugin with an early return, so the rest of the build is unaffected:
public abstract class GuardedConventionPlugin implements Plugin<Project> {
@Inject
protected abstract BuildFeatures getBuildFeatures(); (1)
@Override
public void apply(Project project) {
if (getBuildFeatures().getIsolatedProjects().getActive().get()) { (2)
project.getLogger().warn(
"Skipping " + getClass().getSimpleName() + " on " + project.getPath()
+ ", because Isolated Projects is enabled");
return; (3)
}
configureIncompatibleBuildLogic(project);
}
private void configureIncompatibleBuildLogic(Project project) {
// Build logic that is not (yet) compatible with Isolated Projects.
}
}
| 1 | Inject the BuildFeatures service. |
| 2 | Check whether Isolated Projects is active; it’s safe to call get() on active, because it’s always defined. |
| 3 | Skip the incompatible logic when Isolated Projects is active. |
Guard the application of an incompatible third-party plugin
If a third-party plugin is incompatible with Isolated Projects and is not needed in every workflow, you can guard its application in the same way.
Apply the plugin from a convention plugin rather than from a plugins {} block, so that you can apply it conditionally:
public abstract class ApplierConventionPlugin implements Plugin<Project> {
@Inject
protected abstract BuildFeatures getBuildFeatures();
@Override
public void apply(Project project) {
if (getBuildFeatures().getIsolatedProjects().getActive().get()) { (1)
project.getLogger().warn(
"Not applying com.example.incompatible on " + project.getPath()
+ " because it is incompatible with Isolated Projects");
return;
}
project.getPluginManager().apply("com.example.incompatible");
}
}
| 1 | If Isolated Projects is active, skip applying the incompatible plugin. |
|
Guarding the application of a plugin means the tasks and configuration it contributes are absent when Isolated Projects is active. Any build logic that depends on that plugin must tolerate its absence, or be guarded as well. |
A note for plugin authors
Be especially cautious about shipping such conditionals in a published plugin. A conditional baked into a published plugin changes behavior for every consuming build, and consumers cannot easily see or override it. This multiplies the complexity and divergent-behavior concerns above across the whole ecosystem.
As a plugin author, strongly prefer making the plugin compatible with Isolated Projects over guarding. If you must guard, make the guarded behavior observable (for example, by logging a warning as shown above) and document it clearly, so that consumers are not surprised by build logic that silently changes when Isolated Projects is enabled.
Isolated Projects and other Gradle features
Configuration Cache
Configuration Cache allows skipping the entire configuration phase when no build configuration inputs have changed. However, when any input has changed, the configuration has to be reexecuted in full. When executed, the configuration phase runs sequentially, configuring one project after another.
Isolated Projects relies on the Configuration Cache infrastructure, and it also extends the set of constraints imposed by Configuration Cache.
Configuration Cache is enabled automatically when Isolated Projects is enabled. You should make your build compatible with Configuration Cache first, before migrating to Isolated Projects.
|
It is an error to enable Isolated Projects and explicitly disable Configuration Cache. |
Parallel Configuration Cache
Configuration Cache allows enabling some parallelism in the configuration phase with Parallel Configuration Cache. This requires an opt-in because not all builds are compatible with this behavior, and there is no explicit validation of violations.
With Isolated Projects, Parallel Configuration Cache is enabled automatically, since Isolated Projects constraints guarantee safe and correct results for all builds.
Parallel Execution (--parallel)
Parallel Execution has to do with parallel execution of tasks. This requires an opt-in because not all builds are compatible with this behavior, and there is no explicit validation of violations.
Generally speaking, Parallel Execution is superseded by Configuration Cache because it enables running tasks in parallel even within a single project.
However, Parallel Execution has an additional effect of allowing parallel tooling model building in scenarios like IDE sync. That is why many teams enable it alongside Configuration Cache.
Isolated Projects extends the Configuration Cache, which enables intra-project task parallelism. Additionally, Isolated Projects constraints guarantee safe parallel tooling model building, which allows parallel model building to be enabled by default. Therefore, there is no benefit to enabling Parallel Execution alongside Isolated Projects.
The Parallel Execution flag is ignored when Isolated Projects is enabled.
Configuration on Demand
Configuration on Demand lets Gradle skip configuring projects that aren’t needed for the requested tasks.
Isolated Projects constraints are strong enough to allow a similar optimization. However, the current implementation doesn’t leverage that yet. Instead, all projects are configured in parallel unless the entire configuration phase is skipped on a Configuration Cache hit.
The Configuration on Demand flag is ignored when Isolated Projects is enabled.
Frequently asked questions
Does a complex project dependency graph limit the benefit of Isolated Projects?
No. A common misconception is that Isolated Projects optimizes builds by following dependencies between projects, so that a deep or highly-connected dependency graph would limit the benefit.
The benefit comes from parallelizing the configuration phase. This phase does not require resolving dependencies between projects, so it is not limited by them. The performance is only affected by the number of projects and the cost of configuring them. All projects are always configured regardless of how they depend on each other, and the configuration parallelism is bounded by the project hierarchy (see current limitations).
The benefit varies from build to build, so the best way to find out what you’ll gain is to measure it for your build.