Gradle supports the concept of features: it’s often the case that a single library can be split up into multiple related yet distinct libraries, where each feature can be used alongside the main library.

Features allow a component to expose multiple related libraries, each of which can declare its own dependencies. These libraries are exposed as variants, similar to how the main library exposes variants for its API and runtime.

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

  • a (better) substitute for Maven optional dependencies

  • a main library is built with support for different mutually-exclusive implementations of runtime features; the user must choose one, and only one, implementation of each such feature

  • a main library is built with support for optional runtime features, each of which requires a different set of dependencies

  • a main library comes with supplementary features like test fixtures

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

Selection of features via 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 components 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

  • No two variants in a dependency graph can provide the same capability

  • Multiple variants of a single component may be selected 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 runtime of a component. Then it is allowed as long as the runtime and test fixtures runtime variant of the library declare different capabilities.

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

  • one on the "main" feature, the library

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

While the resolution engine supports multi-variant components independently of the ecosystem, features are currently only available using the Java plugins.

Registering features

Features can be declared by applying the java-library plugin. The following code illustrates how to declare a feature named mongodbSupport:

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

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

java {
    registerFeature('mongodbSupport') {
        usingSourceSet(sourceSets.mongodbSupport)
    }
}

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

Dependency scope configurations are created in the same manner as for the main feature:

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

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

  • the configuration mongodbSupportRuntimeOnly, used to declare runtime-only dependencies for this feature

  • the configuration mongodbSupportCompileOnly, used to declare compile-only dependencies for this feature

  • the configuration mongodbSupportCompileOnlyApi, used to declare compile-only API dependencies for this feature

Furthermore, consumable configurations are created in the same manner as for the main feature:

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

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

A feature should have a source set with the same name. Gradle will create a Jar task to bundle the classes built from the feature source set, using a classifier corresponding to the kebab-case name of the feature.

Do not use the main source set when registering a feature. This behavior will be deprecated in a future version of Gradle.

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

build.gradle.kts
dependencies {
    "mongodbSupportImplementation"("org.mongodb:mongodb-driver-sync:3.9.1")
}
build.gradle
dependencies {
    mongodbSupportImplementation 'org.mongodb:mongodb-driver-sync:3.9.1'
}

By convention, Gradle maps the feature name to a capability whose group and version are the same as the group and version of the main component, respectively, but whose name is the main component name followed by a - followed by the kebab-cased feature name.

For example, if the component’s group is org.gradle.demo, its name is provider, its version is 1.0, and the feature is named mongodbSupport, the feature’s variants will have the org.gradle.demo:provider-mongodb-support:1.0 capability.

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

Publishing features

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

  • using Gradle Module Metadata, everything is published and consumers will get the full benefit of features

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

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

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

build.gradle.kts
plugins {
    `java-library`
    `maven-publish`
}
// ...
publishing {
    publications {
        create("myLibrary", MavenPublication::class.java) {
            from(components["java"])
        }
    }
}
build.gradle
plugins {
    id 'java-library'
    id 'maven-publish'
}
// ...
publishing {
    publications {
        myLibrary(MavenPublication) {
            from components.java
        }
    }
}

Adding javadoc and sources JARs

Similar to the main Javadoc and sources JARs, you can configure the added feature so that it produces JARs for the Javadoc and sources.

build.gradle.kts
java {
    registerFeature("mongodbSupport") {
        usingSourceSet(sourceSets["mongodbSupport"])
        withJavadocJar()
        withSourcesJar()
    }
}
build.gradle
java {
    registerFeature('mongodbSupport') {
        usingSourceSet(sourceSets.mongodbSupport)
        withJavadocJar()
        withSourcesJar()
    }
}

Dependencies on features

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

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

  • with Gradle Module 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:

build.gradle.kts
group = "org.gradle.demo"

sourceSets {
    create("mysqlSupport") {
        java {
            srcDir("src/mysql/java")
        }
    }
}

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

dependencies {
    "mysqlSupportImplementation"("mysql:mysql-connector-java:8.0.14")
}
build.gradle
group = 'org.gradle.demo'

sourceSets {
    mysqlSupport {
        java {
            srcDir 'src/mysql/java'
        }
    }
}

java {
    registerFeature('mysqlSupport') {
        usingSourceSet(sourceSets.mysqlSupport)
    }
}

dependencies {
    mysqlSupportImplementation 'mysql:mysql-connector-java:8.0.14'
}

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

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")
        }
    }
}
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")
        }
    }
}

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

Similarly, if an external library with features was published with Gradle Module Metadata, it is possible to depend on a feature provided by that library:

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")
        }
    }
}
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")
        }
    }
}

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:

No two variants in a dependency graph can provide the same capability

We can leverage this to ensure that Gradle fails whenever the user mis-configures dependencies. Consider a situation where your library supports MySQL, Postgres and MongoDB, but that it’s only allowed to choose one of those at the same time. We can model this restriction by ensuring each feature also provides the same capability, thus making it impossible for these features to be used together in the same graph.

build.gradle.kts
java {
    registerFeature("mysqlSupport") {
        usingSourceSet(sourceSets["mysqlSupport"])
        capability("org.gradle.demo", "producer-db-support", "1.0")
        capability("org.gradle.demo", "producer-mysql-support", "1.0")
    }
    registerFeature("postgresSupport") {
        usingSourceSet(sourceSets["postgresSupport"])
        capability("org.gradle.demo", "producer-db-support", "1.0")
        capability("org.gradle.demo", "producer-postgres-support", "1.0")
    }
    registerFeature("mongoSupport") {
        usingSourceSet(sourceSets["mongoSupport"])
        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
java {
    registerFeature('mysqlSupport') {
        usingSourceSet(sourceSets.mysqlSupport)
        capability('org.gradle.demo', 'producer-db-support', '1.0')
        capability('org.gradle.demo', 'producer-mysql-support', '1.0')
    }
    registerFeature('postgresSupport') {
        usingSourceSet(sourceSets.postgresSupport)
        capability('org.gradle.demo', 'producer-db-support', '1.0')
        capability('org.gradle.demo', 'producer-postgres-support', '1.0')
    }
    registerFeature('mongoSupport') {
        usingSourceSet(sourceSets.mongoSupport)
        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'
}

Here, the producer declares 3 features, 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 features (this also works transitively):

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")
        }
    }
}
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")
        }
    }
}

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