dependencySubstitution on parent project is not getting applied on child project when plugin is applied

I have a multi-project layout which uses git submodules such that developers can checkout the sub projects as standalone git repos and work on them. For eg. I have Parent gradle project which has Child1 and Child2 as sub projects.

Let me discuss the reasoning for that so that the context is laid out.

If Child1 depends on Child2 as a library, I have the Child2 dependency added in Child1 as a maven repo (implementation com.example:child2:1.0.0) so that a developer checking out only Child1 doesn’t have broken dependencies. At the same time, I want people checking out Parent to be able to use the Child2 dependency as a project (implementation project(:child2)). To implement this, I use dependency substitution in Parent only like below

allprojects {
    configurations.all {
        resolutionStrategy {
            dependencySubstitution {
                substitute module('com.example:child2') with project(':child2')
            }
        }
    }
}

The above substitution is not working when Child1 has the spring dependency management plugin active

plugins {
    id 'java'
    id 'eclipse'
    id 'idea'
    id 'org.springframework.boot' version '2.7.5'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

The substitution happens as soon as the spring dependency management plugin is removed. The substitution also works if I use the substitution code in Child1 itself like

configurations.all {
    resolutionStrategy {
        dependencySubstitution {
            substitute module('com.example:child2') with project(':child2')
        }
    }
}

but that’s not something that I am looking to do here.

I am able to reproduce this consistently and have created a repo with a simple reproduction (here I am replacing joda-time with the library project in the consumer, both of which are present in the dep-sub-test project, as I need an existing maven package to demonstrate`)

To reproduce:

Pull the project as is and run .\gradlew.bat :consumer:dependencies --configuration compileClasspath to see the dependency tree. The output will have

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-starter-web -> 2.7.5
|    +--- org.springframework.boot:spring-boot-starter:2.7.5
|    ...spring stuff
\--- joda-time:joda-time:2.12.0

Comment out the lines id 'io.spring.dependency-management' version '1.0.15.RELEASE' and implementation 'org.springframework.boot:spring-boot-starter-web' and run the above command once again. The output will have

compileClasspath - Compile classpath for source set 'main'.
\--- joda-time:joda-time:2.12.0 -> project :library

NOTE: the org.springframework.boot:spring-boot-starter-web dependency is only there to ensure that there is no corner case that is missed when the plugin is added but no spring dependencies are given.


The above is taken from the GitHub issue that I created. After sleeping on the issue, I have been thinking of some way that I can tell the allprojects block in the Parent project to apply the configurations in the block after the configuration in the child projects Child1 and Child2 are done, as to me, it seems like that the plugin is overwriting the substitution. Is there are Gradle only workaround for this?


The developer of the plugin has responded and quoting:

It would appear that Gradle loses the dependency substitution when another resolution strategy configures a version.

I just skimmed over your post, but from what I grasped, you should not use dependency substitutions at all.
As far as I got it, Child1 and Child2 are individual standalone builds, but you also use them as subprojects withinn Parent.
You should never ever ever even try to do that.
Never use one project in more than one build.

What you are after are composite builds, there you do not add Child1 and Child2 as subprojects, but include the whole builds into the Parent build.
The dependency declaration using the coordinates then simply works without any dependency substitution necessary if you configured the projects properly.

1 Like

Yeah! Composite builds is probably what I was looking for. I have been using this ad-hoc multi project build for years now (started when Gradle 5.6 was released). I have reconfigured our project based on the composite build samples and things work out of the box. Thanks a lot for pointing me in the correct direction.

As a note, I had raised an issue in gradle Gradle loses the dependency substitution when another resolution strategy configures a version · Issue #23509 · gradle/gradle · GitHub, based on the response of the plugin developer. Do you think that the issue has any merit to it in isolation? If not, I will close it.

Well, at least the situation you describe in that issue is a combination of various bad practices.
For example using allprojects { ... } doing cross-project configuration instead of using convention plugins, not using composite build where it applies, using the Spring dependency management plugin instead of the built-in BOM support using platform(...), …

But nevertheless it sounds like a bug that should maybe be looked at.
So I’d keep it open and see what the Gradle guys say.
You could maybe add a note that you already learned about composite builds and use them now for your use-case. :slight_smile:

1 Like

I have already reworked our projects to use composite builds and I agree that this is a far better approach.

As for the bad practices that you mentioned, why do you consider the Spring dependency management plugin to be a bad practice. Yes, I know of the BOM support in Gradle but I thought the plugin was better.

It is not. It is just a left-over from the time when Gradle did not have built-in BOM support. To quote Andy Wilkinson from the Spring Boot team:

So the plugin is not only maintained because of the version overwrites. :wink:

@Vampire I’m afraid you’re mistaken. That is the only reason that the plugin is still maintained. Support for Maven-style exclusion semantics will be removed in the future . In fact, it would have already happened if we’d had the bandwidth for it. I’d strongly encourage you to look at using Gradle’s built-in platform support and find an alternative way of dealing with the lack of property-based version overrides.

Source: What is the proper way to apply a BOM in a library project i | Dependency-management | Gradle-community

1 Like

Wow, that thread is enlightening. I too had the same understanding as you, that the plugin did more than just align the versions. But is that’s not true then there is definitely no reason to keep using it. Thank you so much for sharing these insights. Guess I have some simplification to do!

1 Like