What is a Component Metadata Rule in Gradle, and why would you use one?
answer
- patch POM / Module Metadata at resolve time
- components { withModule / all }
- fix deps, add capability, set attribute, belongsTo
- ComponentMetadataRule + @CacheableRule
- cached per module version
basics
~20 sA rule that lets you fix or enrich a published dependency's metadata (its POM/Gradle Module Metadata) at resolution time — without forking the artifact. You register it in the components block to patch wrong or missing info.
solid answer
~40 sPublished artifacts ship metadata (Maven POM or Gradle Module Metadata) describing their dependencies, variants, capabilities and attributes. That metadata is often wrong, incomplete, or pre-dates Gradle's variant model. A **Component Metadata Rule** is a Gradle-side hook that intercepts a module's resolved metadata and lets you patch it locally — add a missing dependency, drop a bogus one, add a capability so Gradle can detect conflicts, attach attributes so variant-aware resolution works, or declare alignment via a virtual platform. You implement `ComponentMetadataRule` and register it under `dependencies { components { ... } }`, targeting one module with `withModule(...)` or every module with `all(...)`. Rules run once per module version and the result is cached, so they're cheap. The win: you correct upstream mistakes in your build instead of pinning/excluding everywhere or vendoring a patched jar.
code
kotlin · 16 lines@CacheableRule
abstract class AddSlf4jRule : ComponentMetadataRule {
override fun execute(context: ComponentMetadataContext) {
context.details.allVariants {
withDependencies {
add("org.slf4j:slf4j-api:1.7.36")
}
}
}
}
dependencies {
components {
withModule("com.example:logging-lib", AddSlf4jRule::class)
}
}go deeper
Know it patches a dependency's metadata locally and lives in the components block; name one use (add a missing dependency).
Explain the four core operations (deps, capabilities, attributes, alignment), withModule vs all, and why it beats exclude/force.
Discuss caching/@CacheableRule, purity, injecting parameters, and when to apply rules in a convention/settings plugin for the whole build.
Frame metadata rules as a governance lever — org-wide correction of broken upstream metadata via a shared plugin, alignment of internal BOMs, and capability normalization to prevent duplicate-library bugs at scale.
## The problem When Gradle resolves `com.example:lib:1.2`, it reads that module's **metadata** — either a Maven `POM`, an Ivy descriptor, or **Gradle Module Metadata** (the `.module` JSON file). The metadata declares the module's dependencies, its variants, its capabilities, and its attributes. Real-world metadata is frequently broken: a POM omits a runtime dependency, declares a dependency that doesn't exist, lacks capability info so Gradle can't detect that two modules are really the same library (e.g. `com.google.collections:google-collections` vs `com.google.guava:guava`), or pre-dates Gradle's variant model entirely. ## What a rule is A **Component Metadata Rule** is your code that Gradle calls while building the in-memory model of a resolved module, *before* it's used for resolution. It implements: ```kotlin @CacheableRule abstract class MyRule : ComponentMetadataRule { override fun execute(context: ComponentMetadataContext) { val details = context.details // mutate details here } } ``` Through `ComponentMetadataContext.details` (a `ComponentMetadataDetails`) you can: - **Fix dependencies** — `details.allVariants { withDependencies { add(...) / removeAll { ... } } }`. - **Add capabilities** — `withCapabilities { addCapability(group, name, version) }` so Gradle can detect conflicts between equivalent modules. - **Set attributes** — `attributes { attribute(...) }` so the module participates in variant-aware resolution (e.g. tag a target JVM version or a status). - **Declare alignment** — `belongsTo("group:virtual-platform:version")` to force a family of modules (e.g. all Jackson modules) to resolve to a single, consistent version. ## Registering rules Rules live in the `components` block: ```kotlin dependencies { components { withModule("com.example:lib", MyRule::class) // one module all(MyRule::class) // every module } } ``` `withModule` scopes the rule to a single `group:name`; `all` runs it against every resolved component (use sparingly — keep it fast and side-effect-free). ## Why not just exclude / force / vendor? `exclude` and forced versions are blunt and must be repeated at every consumer; vendoring a patched jar is a maintenance burden. A metadata rule corrects the *source of truth* (the model Gradle resolves against) once, centrally, and is **cached** — Gradle stores the rule's output keyed by module version, so it doesn't re-run on every build. ## Caching & purity Mark rules `@CacheableRule` and keep `execute` pure (no environment reads, no filesystem). If a rule needs external input, pass it via a constructor `@Inject` parameter so Gradle can track it for cache invalidation.
- When do these rules execute, and is the result cached?They run during dependency resolution, once per module version while Gradle builds the metadata model, before that metadata is used to resolve the graph. Output is cached keyed by the module version (and the rule's tracked inputs), so it doesn't re-run every build.
- Why prefer a metadata rule over a forced version or exclude?Excludes/forces are blunt and must be repeated at every consumer and don't add information (like a missing dep or capability). A rule corrects the model centrally and once, and Gradle can reason about the corrected metadata (conflict detection, alignment) instead of you patching symptoms.
saying these in an interview costs you the question
- Saying a metadata rule rewrites the published artifact or the remote POM — it only affects the local in-memory model Gradle resolves against.
- Claiming rules run per build invocation unconditionally — they're cached per module version.