What is a composite build?
The name itself explains it as a build that includes other builds. In many ways a composite build is similar to a Gradle multi-project build, except that instead of including single projects, complete builds are included. Gradle offers to allow one project to consume artifacts from other builds as if they were part of a single coherent unit. This feature is pretty easy to setup, however you might not know that it’s possible to do the same with Maven.
Composite builds allow you to:
- combine builds that are usually developed independently, for instance when trying out a bug fix in a library that your application uses
- decompose a large multi-project build into smaller, more isolated chunks that can be worked in independently or together as needed
A build that is included in a composite build is referred to, naturally enough, as an “included build“. Included builds do not share any configuration with the composite build, or the other included builds. Each included build is configured and executed in isolation.
Included builds interact with other builds via dependency substitution. If any build in the composite has a dependency that can be satisfied by the included build, then that dependency will be replaced by a project dependency on the included build. Because of the reliance on dependency substitution, composite builds may force configurations to be resolved earlier, when composing the task execution graph. This can have a negative impact on overall build performance, because these configurations are not resolved in parallel.
By default, Gradle will attempt to determine the dependencies that can be substituted by an included build. However for more flexibility, it is possible to explicitly declare these substitutions if the default ones determined by Gradle are not correct for the composite. See Declaring substitutions.
Defining a composite build
The following example demonstrates how builds that are normally developed separately can be combined into a composite build in Gradle and Maven.
Let’s say you have a producer and consumer projects, where producer is set as a dependency on consumer. If these two projects are to have their own release lifecycles their file structure may look like this
The contents of the Gradle build files look like so
And in the case of Maven they look like this
For now, invoking a build for consumer requires pushing a matching version (1.0.0 in this case) of producer to a repository. Let’s enable the composite build feature for both Maven and Gradle. In the case of Gradle we need a new file named settings.gradle in the consumer project; for Maven we need an extra pom.xml file for convenience placed one level up from producer and consumer, like so
There’s only one single change that we have to make in the Gradle build, that is, tell consumer that it should include producer‘s build; this is done by editing settings.gradle like this
Notice the relative path, this is quite important as composite builds rely on source paths. Now, when we run a build on consumer notice that build steps on producer are invoked first as if both projects belonged to multi-project setup
Great! This link between builds can be severed at any time by simply editing settings.gradle once more and removing the includeBuild directive. Now for Maven the additional pom.xml groups both consumer and producer into a single reactor project while still letting those projects keep their individuality, just like we did with Gradle. Here’s how the updated Maven build files look like
Take note that there are no modifications made on consumer and producer, only the new pom.xml file has to be added to the build. With this setup in place we can now invoke targets on consumer from the reactor’s root, like so
Et voilà! Composite builds are a nice feature that enables some use cases like:
- Fixing a bug in producer that’s affecting consumer. An iterative approach may be needed to make the final fix. In a traditional approach you may need to make multiple intermediate releases for producer until the fix can be verified in consumer. composite builds shortens the time it takes to iterate and also reduces the risk of outdated snapshot dependencies on a repository.
- You may want to give monorepo a try but don’t want to change commit history for now. You can “fake” a monorepo by composing all required builds.
- You want to split a monorepo into multiple repositories but some projects may need to be co-located as if they were still part of the monorepo.
- Some other use case where having direct access to sources for all interesting dependencies is a must.
There you go, the way I see it composite builds are a great alternative to publishing intermediate snapshot releases for many cases.