This chapter covers more details on selected topics related to structuring and building a software product with Gradle.

Working with the software project

You have several options to interact with a build that is a composition of multiple builds.

Using an umbrella build

If all your builds are located in one folder structure, you can have an umbrella build in a root folder that includes all builds. You can then call tasks from the root project by addressing one of the builds. Usually, you would then put the Gradle wrapper into the root as well. The sample on structuring software projects contains such an umbrella build in the root. You can address tasks from there:

$ ./gradlew :server-application:app:bootRun

$ ./gradlew :android-app:app:installDebug

The umbrella build is a good place to define cross-build lifecycle tasks. For example, you can define a checkFeatures task for conveniently running all checks in selected components by adding a build.gradle(.kts) file to your umbrella build.

build.gradle.kts
// This is an example of a lifecycle task that crosses build boundaries defined in the umbrella build.
tasks.register("checkFeatures") {
    group = "verification"
    description = "Run all feature tests"
    dependsOn(gradle.includedBuild("admin-feature").task(":config:check"))
    dependsOn(gradle.includedBuild("user-feature").task(":data:check"))
    dependsOn(gradle.includedBuild("user-feature").task(":table:check"))
}
build.gradle
// This is an example of a lifecycle task that crosses build boundaries defined in the umbrella build.
tasks.register('checkFeatures') {
    group = 'verification'
    description = 'Run all feature tests'
    dependsOn(gradle.includedBuild('admin-feature').task(':config:check'))
    dependsOn(gradle.includedBuild('user-feature').task(':data:check'))
    dependsOn(gradle.includedBuild('user-feature').task(':table:check'))
}

In your IDE, you can import the umbrella build and then will have all Gradle builds as projects/modules visible in the workspace.

Working with components in isolation

Independent of whether you have an umbrella build or not, you can work with each component independently. That is, you can pick any component build and build it individually.

In the sample, the umbrella build is a convenience. The whole project can also be used without it, if you work with the components independently.

$ cd server-application
$ ../gradlew :app:bootRun

$ cd android-app
$ ../gradlew :app:installDebug

$ cd user-feature
$ ../gradlew check

You can also import components independently in the IDE. This allows you to focus only on the parts important for the component you work on in your IDE’s workspace. It might also speed up the IDE performance in the case of a very large code base.

If all components live in the same repository, you should only have one Gradle wrapper in the root of the repository. If you have an umbrella build there, you can use that to manage the wrapper.

However, if you import an individual component in an IDE, it might have issues finding the wrapper and you might need to configure a Gradle installation manually.

If your components are scattered over multiple repositories, each should have its own wrapper, but you should ensure that you upgrade them simultaneously.

Using multiple source repositories

Multi-repo development is a well known alternative to mono-repo development. Both have advantages and disadvantages. It depends on many different factors which setup works best for the development of your product.

Gradle aims to support both setups equally well. When you split your product into components, each represented by an independent build, switching a Gradle build between mono- and multi-repo development is simple. In mono-repo development, you put all builds under a common root. In multi-repo development, you place each build into a separate source repository.

Multi-repo development possibly needs some additional guidlines and tooling, so that builds can still find each other. A simple solution is that users who want to build a certain component need to clone all repositories of dependent components next to each other in a file hierarchy. If you follow this pattern, builds can find each other with includeBuild("../other-component") statements. If locations are more flexible, you can also invoke Gradle with --include-build flags to provide locations dynamically.

Another more evolved setup can involve versioning all components and, instead of including the source versions of all components, depend on published versions of them from binary repositories. This is described next.

Publishing and using binary components

You can also decide to publish your components to a binary repository. If you make the decision to do so at some point and you want to work with binary versions of certain components instead of the source versions, you can do that by adding the repository to which you published instead of the corresponding includeBuild("…​") statements in your settings.gradle(.kts) file. If the components keep their coordinates, you do not need to adjust any dependencies. You just need to define versions for the components, ideally in a platform project.

Publishing components with convention plugins

Note that when publishing build logic components, the maven-publish will also publish so called plugin markers that allow Gradle to find plugins by ID – even if they are located in a repository. For that you only need to declare the repositories you want to publish to in your build the same way you do it for other components.

Sharing repository and included build declarations between builds

Each component build has its own settings.gradle(.kts) file to describe the location of other components. Which is done by declaring repositories with binary components and by declaring file system locations of included builds.

If components are developed independently, it often makes sense to define these individually for each one. Then it is individually controlled where the other components originate from. Furthermore, the declarations might vary from build to build. For example, you might only include the builds that are needed to build a certain component and not all builds that make up the product.

However, it may also lead to redundancy as you declare the same repositories and included builds in each settings.gradle(.kts) file again. In particular, if all builds live in the same repository.

Similar as for build scripts, you can define settings convention plugins for the settings.gradle(.kts) file to reuse configuration. For that, you should create a separate build. Settings convention plugins can be written in Groovy DSL or Kotlin DSL similar to other convention plugins. The script file name has to end with .settings.gradle(.kts). A build providing a settings plugin needs to be included as plugin builds in the pluginManagement {} block.