skip to content

How do you publish a component so that two of its optional features are mutually exclusive via capabilities, and what's the difference between feature variants and a capability conflict?

level: principalimportance: nice to knowfreq 18%

answer

  1. registerFeature => capability group:project-feature
  2. outgoing { capability(...) } on consumable config
  3. requireCapability = opt into a feature
  4. same capability on two modules = exclusivity
  5. feature = additive; conflict = mutual exclusion

basics

~20 s

When publishing, declare a capability on a variant via the Java library's registerFeature or by adding capabilities to outgoing configurations. If you give two alternative implementations the same capability, consumers can pick only one — Gradle enforces mutual exclusivity at resolution.

solid answer

~40 s

On the **producer** side you attach capabilities to outgoing (consumable) configurations. The Java ecosystem exposes `java { registerFeature("name") { usingSourceSet(...) } }`, which creates apiElements/runtimeElements-style variants carrying a capability of `group:base-featureName`. For raw control you add a capability directly to a consumable configuration via `outgoing { capability("g:n:v") }`. To model two *alternative* implementations (say `netty` vs `nio` transports) as mutually exclusive, publish them as separate modules/variants that declare the **same** capability. Consumers requesting both then hit a capability conflict and must select one. Distinction: a **feature variant** is a normal, optional, *additive* capability a consumer opts into (`requireCapability`); a **capability conflict** arises only when two providers claim the *same* capability in one graph. Same machinery (capabilities), different intent — opt-in extras vs enforced exclusivity.

code

kotlin · 13 lines
kotlin
// PRODUCER: optional feature variant from a sibling source set
java {
    registerFeature("nettyTransport") {
        usingSourceSet(sourceSets["nettyTransport"])
    }
}

// CONSUMER: opt into that feature variant
dependencies {
    implementation("com.acme:lib:1.0") {
        capabilities { requireCapability("com.acme:lib-nettyTransport") }
    }
}

go deeper

for a junior

Aware that producers can attach capabilities and consumers can require them; deep detail optional.

for a middle

Use registerFeature and requireCapability for an optional feature.

for a senior

Add raw capabilities to outgoing variants and explain how shared capabilities enforce exclusivity.

for a principal

Design published metadata so 'pick one of these alternatives' is enforced by capabilities as org governance, and document the coordinates.

## Producer-side capabilities A **consumable** configuration (one with `canBeConsumed = true`, `canBeResolved = false`) represents a variant other projects can depend on. Each such variant can declare capabilities via its `outgoing` container: ```kotlin configurations { val intransport by creating { isCanBeConsumed = true isCanBeResolved = false outgoing { capability("com.acme:transport:1.0") } } } ``` ## Feature variants (the Java way) For JVM libraries, the idiomatic approach is `registerFeature`: ```kotlin java { registerFeature("nettyTransport") { usingSourceSet(sourceSets["nettyTransport"]) } } ``` This creates `nettyTransportApiElements`/`nettyTransportRuntimeElements` variants, each carrying the capability `group:project-nettyTransport`. A consumer opts in: ```kotlin dependencies { implementation("com.acme:lib") { capabilities { requireCapability("com.acme:lib-nettyTransport") } } } ``` That is **additive**: you're asking for an optional feature. ## Modeling mutual exclusivity To make two implementations exclusive, give them the **same capability**. If `netty-transport` and `nio-transport` are separate modules both declaring `com.acme:transport`, then any graph pulling both gets a **capability conflict** — exactly the enforcement you want for "choose one transport." You publish each with: ```kotlin // in each transport module's build configurations.named("runtimeElements") { outgoing { capability("com.acme:transport:${version}") } } ``` Now the *consumer* must resolve it (capabilitiesResolution) — the producer has encoded the policy that only one transport is valid. ## Feature variant vs capability conflict | Aspect | Feature variant | Capability conflict | |---|---|---| | Intent | Optional add-on a consumer opts into | Two providers of one feature can't coexist | | Capability uniqueness | Distinct capability per feature | **Same** capability on two providers | | Consumer action | `requireCapability(...)` to opt in | `capabilitiesResolution { select(...) }` to choose | | Default if untouched | Feature simply not included | Hard build failure | Both ride on the **same capability machinery** — the difference is whether the capability is *unique* (opt-in extra) or *shared by alternatives* (enforced exclusivity). ## Design implications - Use feature variants to ship optional integrations from one codebase without separate artifacts. - Use shared capabilities across alternative modules to guarantee consumers don't accidentally combine incompatible implementations. - Document the capability coordinate so downstream teams know what to `requireCapability`/resolve. - This is a governance lever: a platform team can encode "these are alternatives, pick one" into published metadata rather than relying on docs.

  • What capability does registerFeature("foo") create by default?
    A capability of the form group:projectName-foo, attached to the generated apiElements/runtimeElements variants for that feature.
  • How does a consumer opt into a feature variant?
    On the dependency, add capabilities { requireCapability("group:name-feature") } so resolution selects the variant carrying that capability.
  • How is a feature variant different from a capability conflict if both use capabilities?
    A feature variant uses a unique capability you opt into (additive); a conflict arises only when two providers share the same capability, making them mutually exclusive and failing by default.

saying these in an interview costs you the question

  • Conflating registerFeature (opt-in optional feature) with capability conflicts (enforced exclusivity) — they share machinery but have opposite intent.
  • Adding capabilities to resolvable configurations — capabilities are declared on consumable/outgoing variants.
  • Assuming consumers automatically get feature variants — they must requireCapability to pull them in.

context