skip to content

Show how a Component Metadata Rule adds or removes dependencies on a published module with a broken POM.

level: middleimportance: must knowfreq 38%

answer

  1. allVariants { withDependencies { add / removeAll } }
  2. withVariant for one variant
  3. add with version { require(...) }
  4. fixes missing & spurious deps centrally
  5. withModule + @CacheableRule

basics

~20 s

In the rule, call details.allVariants { withDependencies { add("g:a:v") } } to add a missing dependency, or removeAll { it.group == "bad" } to drop a bogus one — patching the module's dependency list at resolution time.

solid answer

~40 s

Published POMs often declare too many or too few dependencies — a library forgets to list `slf4j-api` it actually needs, or pulls in a heavyweight optional dependency you must drop. A metadata rule edits the dependency list through `ComponentMetadataDetails`. Inside `execute`, use `details.allVariants { withDependencies { ... } }` (or target one variant with `withVariant("runtime")`). The `withDependencies` block exposes a mutable collection: `add("org.slf4j:slf4j-api:1.7.36")` to add, `add("g:a") { version { require("1.2") } }` to add with a constraint, and `removeAll { it.group == "unwanted" }` to drop. You can also mutate version constraints in place. The change applies to *every* consumer of that module in your build, centrally, instead of repeating `exclude`/extra-dependency declarations at each call site. Always scope precisely with `withModule("the:broken-module", Rule::class)` and mark the rule `@CacheableRule`.

code

kotlin · 15 lines
kotlin
@CacheableRule
abstract class FixDepsRule : ComponentMetadataRule {
    override fun execute(context: ComponentMetadataContext) {
        context.details.allVariants {
            withDependencies {
                add("org.slf4j:slf4j-api:1.7.36")          // missing
                removeAll { it.group == "log4j" }            // spurious
            }
        }
    }
}

dependencies {
    components { withModule("com.example:broken-lib", FixDepsRule::class) }
}

go deeper

for a junior

Know that withDependencies { add(...) } can supply a forgotten dependency.

for a middle

Use allVariants/withVariant, add with constraints, and removeAll, and explain why it beats per-consumer excludes.

for a senior

Discuss variant scoping, version-constraint hygiene (require/prefer vs hard pin), and caching.

for a principal

Ship such fixes via a shared plugin so all teams inherit corrected metadata; track upstream fixes to retire rules over time.

## Two flavors of broken dependency metadata 1. **Missing dependency** — the artifact uses a library at runtime but the POM doesn't declare it (common when the publisher marked it `provided`/optional or just forgot). Consumers get `ClassNotFoundException` at runtime. 2. **Spurious dependency** — the POM declares something you don't want: an optional feature jar, a conflicting logging backend, or a fat transitive you intend to exclude everywhere. Both are fixable in one place with a metadata rule instead of scattering `exclude`/extra `implementation` lines. ## The API ```kotlin @CacheableRule abstract class FixDepsRule : ComponentMetadataRule { override fun execute(context: ComponentMetadataContext) { context.details.allVariants { withDependencies { // add a missing dependency add("org.slf4j:slf4j-api:1.7.36") // add with a version constraint add("com.example:helper") { version { require("2.0") } } // drop a bogus dependency removeAll { it.group == "log4j" && it.name == "log4j" } } } } } dependencies { components { withModule("com.example:broken-lib", FixDepsRule::class) } } ``` - **`allVariants`** patches every variant of the module; **`withVariant("runtime")`** or `withVariant("compile")` scopes the change to one variant (e.g. add a runtime-only dependency). - **`withDependencies`** gives a `DirectDependenciesMetadata` collection you can `add`, `removeAll`, or iterate to tweak `version { ... }`. - For modules with only a POM (Maven), Gradle derives `compile`/`runtime` variants, so `allVariants` is the safe default. ## Why this beats exclude / extra deps at the call site - **`exclude`** at a consumer only removes a transitive locally and must be repeated everywhere the module appears; it also can't *add* anything. - Adding an `implementation` dependency to compensate for a missing one doesn't fix the broken module's own metadata, so other consumers (and the module's published variants) stay wrong. A metadata rule corrects the module's model itself, so every path that resolves it sees the fix, and the result is cached. ## Caveats - Keep `add` versions sensible — prefer `require`/`prefer` constraints over hard strings so you don't accidentally pin. - Don't over-broaden with `all`; a dependency fix is module-specific, so use `withModule`. - The rule changes resolution, not the artifact — the jar's own embedded metadata (if any) is untouched.

  • How is removing a dependency in a rule different from an `exclude` at the consumer?
    A rule edits the module's own dependency metadata once, so every consumer in the build sees it removed; an `exclude` is local to one declaration and must be repeated everywhere the module is pulled in.
  • How do you add a dependency to only the runtime variant?
    Use `withVariant("runtime") { withDependencies { add(...) } }` instead of `allVariants`, so the dependency is added to the runtime classpath only and not the compile classpath.

saying these in an interview costs you the question

  • Adding a compensating `implementation` dependency instead of fixing the module — leaves the module's metadata wrong for other consumers.
  • Using `all` for a single-module dependency fix.

context