In a dependency graph, it’s common for multiple implementations of the same API to be accidentally included, especially with libraries like logging frameworks where different bindings are selected by various transitive dependencies.

Since these implementations typically reside at different group, artifact, and version (GAV) coordinates, build tools often can’t detect the conflict.

To address this, Gradle introduces the concept of capability.

Understanding capabilities

A capability is essentially a way to declare that different components (dependencies) offer the same functionality.

It’s illegal for Gradle to include more than one component providing the same capability in a single dependency graph. If Gradle detects two components providing the same capability (e.g., different bindings for a logging framework), it will fail the build with an error, indicating the conflicting modules. This ensures that conflicting implementations are resolved, avoiding issues on the classpath.

For instance, suppose you have dependencies on two different libraries for database connection pooling:

dependencies {
    implementation("com.zaxxer:HikariCP:4.0.3")  // A popular connection pool
    implementation("org.apache.commons:commons-dbcp2:2.8.0")  // Another connection pool
}

configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("database:connection-pool") {
        select("com.zaxxer:HikariCP")
    }
}

In this case, both HikariCP and commons-dbcp2 provide the same functionality (connection pooling). Gradle will fail if both are on the classpath.

Since only one should be used, Gradle’s resolution strategy allows you to select HikariCP, resolving the conflict.

Understanding capability coordinates

A capability is identified by a (group, module, version) triplet.

Every component defines an implicit capability based on its GAV coordinates: group, artifact, and version.

For instance, the org.apache.commons:commons-lang3:3.8 module has an implicit capability with the group org.apache.commons, name commons-lang3, and version 3.8:

dependencies {
    implementation("org.apache.commons:commons-lang3:3.8")
}

It’s important to note that capabilities are versioned.

Declaring component capabilities

To detect conflicts early, it’s useful to declare component capabilities through rules, allowing conflicts to be caught during the build instead of at runtime.

One common scenario is when a component is relocated to different coordinates in a newer release.

For example, the ASM library was published under asm:asm until version 3.3.1, and then relocated to org.ow2.asm:asm starting with version 4.0. Including both versions on the classpath is illegal because they provide the same feature, under different coordinates.

Since each component has an implicit capability based on its GAV coordinates, we can address this conflict by using a rule that declares the asm:asm module as providing the org.ow2.asm:asm capability:

build.gradle.kts
class AsmCapability : ComponentMetadataRule {
    override
    fun execute(context: ComponentMetadataContext) = context.details.run {
        if (id.group == "asm" && id.name == "asm") {
            allVariants {
                withCapabilities {
                    // Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
                    addCapability("org.ow2.asm", "asm", id.version)
                }
            }
        }
    }
}
build.gradle
@CompileStatic
class AsmCapability implements ComponentMetadataRule {
    void execute(ComponentMetadataContext context) {
        context.details.with {
            if (id.group == "asm" && id.name == "asm") {
                allVariants {
                    it.withCapabilities {
                        // Declare that ASM provides the org.ow2.asm:asm capability, but with an older version
                        it.addCapability("org.ow2.asm", "asm", id.version)
                    }
                }
            }
        }
    }
}

With this rule in place, the build will fail if both asm:asm ( < = 3.3.1) and org.ow2.asm:asm (4.0+) are present in the dependency graph.

Gradle won’t resolve the conflict automatically, but this helps you realize that the problem exists. It’s recommended to package such rules into plugins for use in builds, allowing users to decide which version to use or to fix the classpath conflict.

Selecting between candidates

At some point, a dependency graph is going to include either incompatible modules, or modules which are mutually exclusive.

For example, you may have different logger implementations, and you need to choose one binding. Capabilities help understand the conflict, then Gradle provides you with tools to solve the conflicts.

Selecting between different capability candidates

In the relocation example above, Gradle was able to tell you that you have two versions of the same API on classpath: an "old" module and a "relocated" one. We can solve the conflict by automatically choosing the component which has the highest capability version:

build.gradle.kts
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("org.ow2.asm:asm") {
        selectHighestVersion()
    }
}
build.gradle
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability('org.ow2.asm:asm') {
        selectHighestVersion()
    }
}

However, choosing the highest capability version conflict resolution is not always suitable.

For a logging framework, for example, it doesn’t matter what version of the logging frameworks we use. In this case, we explicitly select slf4j as the preferred option:

build.gradle.kts
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
        val toBeSelected = candidates.firstOrNull { it.id.let { id -> id is ModuleComponentIdentifier && id.module == "log4j-over-slf4j" } }
        if (toBeSelected != null) {
            select(toBeSelected)
        }
        because("use slf4j in place of log4j")
    }
}
build.gradle
configurations.all {
    resolutionStrategy.capabilitiesResolution.withCapability("log4j:log4j") {
        def toBeSelected = candidates.find { it.id instanceof ModuleComponentIdentifier && it.id.module == 'log4j-over-slf4j' }
        if (toBeSelected != null) {
            select(toBeSelected)
        }
        because 'use slf4j in place of log4j'
    }
}

This approach works also well if you have multiple slf4j bindings on the classpath; bindings are basically different logger implementations, and you need only one. However, the selected implementation may depend on the configuration being resolved.

For instance, in testing environments, the lightweight slf4j-simple logging implementation might be sufficient, while in production, a more robust solution like logback may be preferable.

Resolution can only be made in favor of a module that is found in the dependency graph. The select method accepts only a module from the current set of candidates. If the desired module is not part of the conflict, you can choose not to resolve that particular conflict, effectively leaving it unresolved. Another conflict in the graph may have the module you want to select.

If no resolution is provided for all conflicts on a given capability, the build will fail because the module chosen for resolution was not found in the graph. Additionally, calling select(null) will result in an error and should be avoided.

For more information, refer to the capabilities resolution API.