Gradle supports the concept of feature variants: when building a library, it’s often the case that some features should only available when some dependencies are present, or when special artifacts are used.

Feature variants let consumers choose what features of a library they need: the dependency management engine will select the right artifacts and dependencies.

This allows a number of different scenarios (list is non exhaustive):

  • a (better) substitute to Maven optional dependencies

  • a main library is built with support for different runtime features, and the user has to choose between one of them

  • a main library is built with support for different runtime features, each of them requiring a different set of dependencies

  • a main library comes with secondary variants like test fixtures

  • a main library comes with a main artifact, and enabling an additional feature requires additional artifacts

Selection of feature variants and capabilities

Declaring a dependency on a component is usually done by providing a set of coordinates (group, artifact, version also known as GAV coordinates). This allows the engine to determine the component we’re looking for, but such a component may provide different variants. A variant is typically chosen based on the usage. For example, we might choose a different variant for compiling against a component (in which case we need the API of the component) or when executing code (in which case we need the runtime of the component). All variants of a component provide a number of capabilities, which are denoted similarly using GAV coordinates.

A capability is denoted by GAV coordinates, but you must think of it as feature description:

  • "I provide an SLF4J binding"

  • "I provide runtime support for MySQL"

  • "I provide a Groovy runtime"

And in general, having two component that provide the same thing in the graph is a problem (they conflict).

This is an important concept because:

  • by default a variant provides a capability corresponding to the GAV coordinates of its component

  • it is not allowed to have different components or different variants of a component in a dependency graph if they provide the same capability

  • it is allowed to select two variants of the same component, as long as they provide different capabilities

A typical component will only provide variants with the default capability. A Java library, for example, exposes two variants (API and runtime) which provide the same capability. As a consequence, it is an error to have both the API and runtime of a single component in a dependency graph.

However, imagine that you need the runtime and the test fixtures of a component. Then it is allowed as long as the runtime and test fixtures variant of the library declare different capabilities.

If we do so, a consumer would then have to declare two dependencies:

  • one on the "main" variant, the library

  • one on the "test fixtures" variant, by requiring its capability

While the engine supports feature variants independently of the the ecosystem, this feature is currently only available using the Java plugins and is incubating.

Declaring feature variants

Feature variants can be declared by applying the java or java-library plugins. The following code illustrates how to declare a feature named mongodbSupport:

Example 1. Declaring a feature variant
build.gradle
group = 'org.gradle.demo'
version = '1.0'

java {
    registerFeature('mongodbSupport') {
        usingSourceSet(sourceSets.main)
    }
}
build.gradle.kts
group = "org.gradle.demo"
version = "1.0"

java {
    registerFeature("mongodbSupport") {
        usingSourceSet(sourceSets["main"])
    }
}

Gradle will automatically setup a number of things for you, in a very similar way to how the Java Library Plugin sets up configurations:

  • the configuration mongodbSupportApi, used to declare API dependencies for this feature

  • the configuration mongodbSupportImplementation, used to declare implementation dependencies for this feature

  • the configuration mongodyDbSupportApiElements, used by consumers to fetch the artifacts and API dependencies of this feature

  • the configuration mongodyDbSupportRuntimeElements, used by consumers to fetch the artifacts and runtime dependencies of this feature

Most users will only need to care about the first two configurations, to declare the specific dependencies of this feature:

Example 2. Declaring dependencies of a feature
build.gradle
dependencies {
    mongodbSupportImplementation 'org.mongodb:mongodb-driver-sync:3.9.1'
}
build.gradle.kts
dependencies {
    "mongodbSupportImplementation"("org.mongodb:mongodb-driver-sync:3.9.1")
}

By convention, Gradle will map the feature name to a capability which group is the same as the group and version as the main component, but a name composed of the main component name and the kebab-cased feature name.

For example, if the group is org.gradle.demo, the name of the component is provider, its version is 1.0 and the feature is named mongodbSupport, so the feature variant will be org.gradle.demo:provider-mongodb-support:1.0.

If you choose the capability name yourself or add more capabilities to a variant, it is recommended to follow the same convention.

Feature variant source set

In the previous example, we’re declaring a feature variant which uses the main source set. This is a typical use case in the Java ecosystem, where it’s, for whatever reason, not possible to split the sources of a project into different subprojects or different source sets. Gradle will therefore declare the configurations as described, but will also setup the compile classpath and runtime classpath of the main source set so that it extends from the feature configuration. Said differently, this allows you to declare the dependencies specific to a feature in their own "bucket", but everything is still compiled as a single source set. There will also be a single artifact (the component Jar) including support for all features.

However, it is often preferred to have a separate source set for a feature. Gradle will then perform a similar mapping, but will not make the compile and runtime classpath of the main component extend from the dependencies of the registered features. It will also, by convention, create a Jar task to bundle the classes built from this feature source set, using a classifier corresponding to the kebab-case name of the feature:

Example 3. Declaring a feature variant using a separate source set
build.gradle
sourceSets {
    mongodbSupport {
        java {
            srcDir 'src/mongodb/java'
        }
    }
}

java {
    registerFeature('mongodbSupport') {
        usingSourceSet(sourceSets.mongodbSupport)
    }
}
build.gradle.kts
sourceSets {
    create("mongodbSupport") {
        java {
            srcDir("src/mongodb/java")
        }
    }
}

java {
    registerFeature("mongodbSupport") {
        usingSourceSet(sourceSets["mongodbSupport"])
    }
}

Publishing feature variants

Depending on the metadata file format, publishing feature variants may be lossy:

  • using POM metadata (Maven), feature variants are published as optional dependencies and artifacts of feature variants are published with different classifiers

  • using Ivy metadata, feature variants are published as extra configurations, which are not extended by the default configuration

  • using experimental Gradle metadata, everything is published and consumers will get the full benefit of feature variants

Publishing feature variants is supported using the maven-publish and ivy-publish plugins only. The Java Plugin (or Java Library Plugin) will take care of registering the additional variants for you, so there’s no additional configuration required, only the regular publications:

Example 4. Publishing a component with feature variants
build.gradle
plugins {
    id 'java-library'
    id 'maven-publish'
}
// ...
publishing {
    publications {
        myLibrary(MavenPublication) {
            from components.java
        }
    }
}
build.gradle.kts
plugins {
    `java-library`
    `maven-publish`
}
// ...
publishing {
    publications {
        create("myLibrary", MavenPublication::class.java) {
            from(components["java"])
        }
    }
}

Dependencies on feature variants

As mentioned earlier, feature variants can be lossy when published. As a consequence, a consumer can depend on a feature variant only in these cases:

  • with a project dependency (in a multi-project build)

  • with Gradle metadata available, that is the publisher MUST have published it

  • within the Ivy world, by declaring a dependency on the configuration matching the feature

A consumer can specify that it needs a specific feature of a producer by declaring required capabilities. For example, if a producer declares a "MySQL support" feature like this:

Example 5. A library declaring a feature to support MySQL
build.gradle
java {
    registerFeature('mysqlSupport') {
        usingSourceSet(sourceSets.main)
    }
}

dependencies {
    mysqlSupportImplementation 'mysql:mysql-connector-java:8.0.14'
}
build.gradle.kts
java {
    registerFeature("mysqlSupport") {
        usingSourceSet(sourceSets["main"])
    }
}

dependencies {
    "mysqlSupportImplementation"("mysql:mysql-connector-java:8.0.14")
}

Then the consumer can declare a dependency on the MySQL support feature by doing this:

Example 6. Consuming specific features in a multi-project build
build.gradle
dependencies {
    // This project requires the main producer component
    implementation(project(":producer"))

    // But we also want to use its MySQL support
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-mysql-support")
        }
    }
}
build.gradle.kts
dependencies {
    // This project requires the main producer component
    implementation(project(":producer"))

    // But we also want to use its MySQL support
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-mysql-support")
        }
    }
}

This will automatically bring the mysql-connector-java dependency on runtime classpath. If there were more than one dependencies, all of them would be brought, meaning that a feature can be used to group dependencies which contribute to a feature together.

Similarly, if the experimental Gradle metadata feature is enabled, it is possible to depend on an external dependency providing feature variants:

Example 7. Consuming specific features from an external repository
build.gradle
dependencies {
    // This project requires the main producer component
    implementation('org.gradle.demo:producer:1.0')

    // But we also want to use its MongoDB support
    runtimeOnly('org.gradle.demo:producer:1.0') {
        capabilities {
            requireCapability("org.gradle.demo:producer-mongodb-support")
        }
    }
}
build.gradle.kts
dependencies {
    // This project requires the main producer component
    implementation("org.gradle.demo:producer:1.0")

    // But we also want to use its MongoDB support
    runtimeOnly("org.gradle.demo:producer:1.0") {
        capabilities {
            requireCapability("org.gradle.demo:producer-mongodb-support")
        }
    }
}

Handling mutually exclusive variants

The main advantage of using capabilities as a way to handle features is that you can precisely handle compatibility of variants. The rule is simple:

It’s not allowed to have two variants of components that provide the same capability in a single dependency graph.

We can leverage that to ask Gradle to fail whenever the user mis-configures dependencies. Imagine, for example, that your library supports MySQL, Postgres and MongoDB, but that it’s only allowed to choose one of those at the same time. Not allowed should directly translate to "provide the same capability", so there must be a capability provided by all three features:

Example 8. A producer of multiple features that are mutually exclusive
build.gradle
java {
    registerFeature('mysqlSupport') {
        usingSourceSet(sourceSets.main)
        capability('org.gradle.demo', 'producer-db-support', '1.0')
        capability('org.gradle.demo', 'producer-mysql-support', '1.0')
    }
    registerFeature('postgresSupport') {
        usingSourceSet(sourceSets.main)
        capability('org.gradle.demo', 'producer-db-support', '1.0')
        capability('org.gradle.demo', 'producer-postgres-support', '1.0')
    }
    registerFeature('mongoSupport') {
        usingSourceSet(sourceSets.main)
        capability('org.gradle.demo', 'producer-db-support', '1.0')
        capability('org.gradle.demo', 'producer-mongo-support', '1.0')
    }
}

dependencies {
    mysqlSupportImplementation 'mysql:mysql-connector-java:8.0.14'
    postgresSupportImplementation 'org.postgresql:postgresql:42.2.5'
    mongoSupportImplementation 'org.mongodb:mongodb-driver-sync:3.9.1'
}
build.gradle.kts
java {
    registerFeature("mysqlSupport") {
        usingSourceSet(sourceSets["main"])
        capability("org.gradle.demo", "producer-db-support", "1.0")
        capability("org.gradle.demo", "producer-mysql-support", "1.0")
    }
    registerFeature("postgresSupport") {
        usingSourceSet(sourceSets["main"])
        capability("org.gradle.demo", "producer-db-support", "1.0")
        capability("org.gradle.demo", "producer-postgres-support", "1.0")
    }
    registerFeature("mongoSupport") {
        usingSourceSet(sourceSets["main"])
        capability("org.gradle.demo", "producer-db-support", "1.0")
        capability("org.gradle.demo", "producer-mongo-support", "1.0")
    }
}

dependencies {
    "mysqlSupportImplementation"("mysql:mysql-connector-java:8.0.14")
    "postgresSupportImplementation"("org.postgresql:postgresql:42.2.5")
    "mongoSupportImplementation"("org.mongodb:mongodb-driver-sync:3.9.1")
}

Where, the producer declares 3 variants, one for each database runtime support:

  • mysql-support provides both the db-support and mysql-support capabilities

  • postgres-support provides both the db-support and postgres-support capabilities

  • mongo-support provides both the db-support and mongo-support capabilities

Then if the consumer tries to get both the postgres-support and mysql-support like this (this also works transitively):

Example 9. A consumer trying to use 2 incompatible variants at the same time
build.gradle
dependencies {
    implementation(project(":producer"))

    // Let's try to ask for both MySQL and Postgres support
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-mysql-support")
        }
    }
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-postgres-support")
        }
    }
}
build.gradle.kts
dependencies {
    // This project requires the main producer component
    implementation(project(":producer"))

    // Let's try to ask for both MySQL and Postgres support
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-mysql-support")
        }
    }
    runtimeOnly(project(":producer")) {
        capabilities {
            requireCapability("org.gradle.demo:producer-postgres-support")
        }
    }
}

Dependency resolution would fail with the following error:

Cannot choose between
   org.gradle.demo:producer:1.0 variant mysqlSupportRuntimeElements and
   org.gradle.demo:producer:1.0 variant postgresSupportRuntimeElements
   because they provide the same capability: org.gradle.demo:producer-db-support:1.0