Cryptic 'no matching variant' errors and multi-flavor Android or multiplatform libraries all come down to this model.
Gradle dependencies are not just coordinates: each resolved module exposes several variants (for a Java library: apiElements, runtimeElements, sourcesElements, javadocElements), and a consuming configuration selects one by matching attributes.
Standard attributes include org.gradle.usage (java-api vs java-runtime), org.gradle.category (library, platform, documentation), org.gradle.libraryelements (jar, classes, resources), org.gradle.jvm.version (the target JVM) and, for Kotlin Multiplatform and Android, platform and build-type attributes. compileClasspath asks for java-api + classes when possible, which is why compilation can consume class directories of sibling projects without building jars.
The rules are published in Gradle Module Metadata (.module files) next to POMs, so variant selection also works for published libraries. A Maven POM is mapped to two synthetic variants.
Common errors and fixes:
- "No matching variant" usually means the producer targets a newer JVM (
org.gradle.jvm.version=21) than the consumer's toolchain, or an Android/KMP library lacks a variant for your platform. Align toolchains or add the missing target. - Ambiguity errors appear when two variants match equally; add a disambiguation rule or request a more specific attribute.
Capabilities declare what a variant provides; two modules providing the same capability conflict (slf4j-simple vs logback-classic style bindings). Feature variants (java { registerFeature("mongo") { usingSourceSet(...) } }) publish optional features with their own dependencies, consumed with requireCapabilities.
Debug with ./gradlew outgoingVariants (what a project offers) and ./gradlew resolvableConfigurations (what it asks for), and dependencyInsight, which prints the selected variant and attributes.
Example
plugins { `java-library` }
repositories { mavenCentral() }
java {
toolchain { languageVersion = JavaLanguageVersion.of(21) }
registerFeature("mongoSupport") { // optional feature variant with its own deps
usingSourceSet(sourceSets["main"])
}
}
dependencies {
"mongoSupportImplementation"("org.mongodb:mongodb-driver-sync:5.3.1")
// consume a sibling's optional feature:
implementation(project(":storage")) {
capabilities { requireCapability("com.acme:storage-mongo-support") }
}
}
// Force a specific target JVM attribute on a custom configuration
val tools by configurations.creating {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17)
}
}
// $ ./gradlew outgoingVariants # apiElements, runtimeElements, mongoSupportApiElements...
// $ ./gradlew resolvableConfigurations --configuration compileClasspath
// $ ./gradlew dependencyInsight --dependency storage --configuration compileClasspathRemember
- Modules expose variants; configurations select one by matching attributes
- usage, category, libraryelements and jvm.version are the key attributes
- 'No matching variant' is usually a JVM target or platform mismatch
- outgoingVariants and resolvableConfigurations show both sides