Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

In this tutorial, we’ll see how we can configure conditional dependencies in our Gradle projects.

2. Project Setup

We’ll be setting up a multi-module project for the demonstration. Let’s head over to start.spring.io and create our root project conditional-dependency-demo. We’ll use Gradle and Java along with Spring Boot.

Let’s also add two provider modules, provider1 and provider2, and two consumer modules, consumer1 and consumer2:
conditional-dependency-project-structure-demo

3. Configuring Conditional Dependency

Let’s say, based on a project property, we want to include one of the two provider modules. For our consumer1 module, we want to include the provider1 module if the property isLocal is specified. Otherwise, the provider2 module should be included.

To do this, let’s add the following in the gradle.settings.kts file of the consumer1 module:

plugins {
    id("java")
}

group = "com.baeldung.gradle"
version = "0.0.1-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0")
    testRuntimeOnly ("org.junit.jupiter:junit-jupiter-engine:5.7.0")

    if (project.hasProperty("isLocal")) {
        implementation("com.baeldung.gradle:provider1")
    } else {
        implementation("com.baeldung.gradle:provider2")
    }
}

tasks.getByName<Test>("test") {
    useJUnitPlatform()
}

Now, let’s run the dependencies task to see which provider module is being picked:

gradle -PisLocal dependencies --configuration implementation
> Task :consumer1:dependencies

------------------------------------------------------------
Project ':consumer1'
------------------------------------------------------------

implementation - Implementation only dependencies for source set 'main'. (n)
\--- com.baeldung.gradle:provider1 (n)

(n) - Not resolved (configuration is not meant to be resolved)

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 591ms
1 actionable task: 1 executed

As we can see, passing the property led to the inclusion of the provider1 module. Let’s now run the dependencies task without any property specified:

gradle dependencies --configuration implementation
> Task :consumer1:dependencies

------------------------------------------------------------
Project ':consumer1'
------------------------------------------------------------

implementation - Implementation only dependencies for source set 'main'. (n)
\--- com.baeldung.gradle:provider2 (n)

(n) - Not resolved (configuration is not meant to be resolved)

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 649ms
1 actionable task: 1 executed

As we can see, provider2 is now being included.

4. Configuring Conditional Dependency via Module Substitution

Let’s look at another approach to conditionally configure the dependency via dependency substitution. For our consumer2 module, we want to include the provider2 module if the isLocal property is specified. Otherwise, module provider1 should be used.

Let’s add the following configuration to our consumer2 module to achieve this goal:

plugins {
    id("java")
}

group = "com.baeldung.gradle"
version = "0.0.1-SNAPSHOT"

repositories {
    mavenCentral()
}

configurations.all {
    resolutionStrategy.dependencySubstitution {
        if (project.hasProperty("isLocal"))
            substitute(project("com.baeldung.gradle:provider1"))
              .using(project(":provider2"))
              .because("Project property override(isLocal).")
    }
}

dependencies {
    implementation(project(":provider1"))

    testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0")
}

tasks.getByName<Test>("test") {
    useJUnitPlatform()
}

Now, if we run the same commands again, we should get similar results. Let’s first run with the isLocal property specified:

gradle -PisLocal dependencies --configuration compilePath
> Task :consumer2:dependencies

------------------------------------------------------------
Project ':consumer2'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
\--- project :provider1 -> project :provider2

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

And sure enough, we see the provider1 project being substituted by the provider2 project. Let’s now try this without the property specified:

gradle dependencies --configuration compilePath
> Task :consumer2:dependencies

------------------------------------------------------------
Project ':consumer2'
------------------------------------------------------------

compileClasspath - Compile classpath for source set 'main'.
\--- project :provider1

A web-based, searchable dependency report is available by adding the --scan option.

BUILD SUCCESSFUL in 623ms
1 actionable task: 1 executed

As expected, no substitution took place this time, and provider1 was included.

5. Difference Between the Two Approaches

As we saw in the demonstrations above, both approaches helped us achieve our goal of conditionally configuring dependencies. Let’s talk about some of the differences between the two approaches.

First, writing the conditional logic directly looks simpler with less configuration as compared to the second approach.

Secondly, although the second approach involved more configuration, it seems more idiomatic. In the second approach, we make use of the substitution mechanism provided by Gradle itself. It also allows us to specify a reason for substitution as well. Moreover, in the logs, we can notice the substitution taking place, unlike in the first approach where no such information is available:

compileClasspath - Compile classpath for source set 'main'. 
\--- project :provider1 -> project :provider2

Let’s also notice that in the first approach, dependency resolution wasn’t required. We can get the results with:

gradle -PisLocal dependencies --configuration implementation

While in the second approach, if we were to check the implementation configuration, we won’t see the expected results. The reason being it only works when the dependency resolution takes place. Hence, it was available with compilePath configuration:

gradle -PisLocal dependencies --configuration compilePath

6. Conclusion

With that, we can conclude this article. In this article, we saw two approaches to configure dependencies conditionally in Gradle. We also analyzed the difference between the two.

The dependency substitution configuration provided with Gradle appeared to be the more idiomatic approach. As always, the complete code and Gradle configurations are available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.