What does `@CacheableRule` mean for a Component Metadata Rule, and how do you keep a rule cache-correct while passing external inputs?
answer
- @CacheableRule = output cached cross-build
- must be pure function of inputs
- no env/file/clock reads
- @Inject params + params(...) in cache key
- RepositoryResourceAccessor for extra descriptors
basics
~20 s@CacheableRule tells Gradle it may cache the rule's output across builds. The rule must be a pure function of its inputs — no env/file reads. To use external data, inject it via an @Inject constructor parameter so Gradle tracks it for invalidation.
solid answer
~50 sComponent metadata rules run during resolution and their results are stored. `@CacheableRule` opts the rule into Gradle's metadata cache: its computed effect on a module version is persisted and reused on later builds (and across the cross-build metadata cache), so the rule body isn't re-executed every time. For caching to be *correct*, the rule must be a deterministic function of: the module's metadata plus any **declared inputs**. That means no reading `System.getenv`, no filesystem access, no clock — anything that varies invisibly would make a cached result stale. When a rule genuinely needs external configuration (a version to inject, a flag, a `RepositoryResourceAccessor` to fetch an extra descriptor), you pass it through an `@Inject` constructor parameter, supplying the value via the `params(...)` argument at registration: `withModule("g:a", MyRule::class) { params("1.7.36") }`. Gradle then includes those parameters in the cache key, so changing a param correctly invalidates the cached result. Skipping `@CacheableRule` is legal but forces the rule to re-run constantly and is discouraged for anything non-trivial.
code
kotlin · 16 lines@CacheableRule
abstract class VersionedDepRule @Inject constructor(
private val version: String,
) : ComponentMetadataRule {
override fun execute(context: ComponentMetadataContext) {
context.details.allVariants {
withDependencies { add("org.slf4j:slf4j-api:$version") }
}
}
}
dependencies {
components {
withModule("com.example:lib", VersionedDepRule::class) { params("1.7.36") }
}
}go deeper
Know that rules can be cached and should be simple/deterministic.
Explain @CacheableRule means pure-function-of-inputs and that external data goes through injected params.
Detail the cache key (metadata + params), RepositoryResourceAccessor, and why env/file reads are forbidden.
Set conventions that all org rules are class-based, cacheable, and parameter-injected; reason about cross-build metadata cache invalidation and reproducibility.
## Why caching matters Metadata rules execute as part of building each resolved module's model. Dependency graphs have hundreds of modules; re-running rules on every build (and there's a cross-build cache) would be wasteful. Gradle caches rule *outputs* so a module version processed once needn't be reprocessed. ## The `@CacheableRule` contract ```kotlin @CacheableRule abstract class VersionedDepRule @Inject constructor( private val slf4jVersion: String ) : ComponentMetadataRule { override fun execute(context: ComponentMetadataContext) { context.details.allVariants { withDependencies { add("org.slf4j:slf4j-api:$slf4jVersion") } } } } ``` Marking the class `@CacheableRule` promises Gradle the rule is a **pure function** of: 1. the resolved module's metadata, and 2. its declared inputs (constructor `@Inject` params, and any injected services it asks for). Given those, the output is deterministic, so Gradle may cache it. Violating purity — reading an env var, a file, the wall clock, or a mutable global — produces stale results because those inputs aren't in the cache key. ## Passing external inputs the cache-safe way Don't capture configuration from the surrounding script (that's invisible to the cache). Instead inject it: ```kotlin dependencies { components { withModule("com.example:lib", VersionedDepRule::class) { params("1.7.36") // becomes part of the cache key } } } ``` Gradle serializes the `params` into the cache key, so changing `"1.7.36"` to `"2.0.0"` invalidates the stale entry. Acceptable injected types are limited to serializable values and a few Gradle services. ## Injectable services A rule can `@Inject` certain Gradle services and still be cacheable, notably: - **`RepositoryResourceAccessor`** — fetch an *additional* descriptor file (e.g. a sibling `pom`/`xml`) from the same repository to derive metadata. Because the fetched resource lives in the repo and is content-addressed, Gradle can track it for caching. - **`ObjectFactory`** for creating attribute/named objects. ## Non-cacheable rules If you omit `@CacheableRule`, the rule still works but Gradle treats it as non-cacheable and re-evaluates it more aggressively, and inline-action rules (`withModule("g:a") { ... }`) can't be cacheable at all. For anything beyond a throwaway one-liner, use a `@CacheableRule` class. ## Practical checklist - Class-based + `@CacheableRule`. - No `System.getenv`, no `File(...)`, no `Instant.now()`. - External data via `@Inject` constructor params + `params(...)`. - Extra repo descriptors via `RepositoryResourceAccessor`, not raw HTTP.
- What happens if a `@CacheableRule` reads an environment variable?It breaks the cache contract: the env var isn't part of the cache key, so a cached result can be stale when the env changes. Pass such values via `@Inject` params instead so they're tracked.
- How would a rule fetch an extra metadata file from the repository safely?Inject `RepositoryResourceAccessor` and use it to read a sibling resource from the same repo. The access is repository-scoped and trackable, so the rule stays cacheable — unlike a raw HTTP call.
- Can an inline-action rule be cacheable?No. `withModule("g:a") { ... }` inline actions cannot be `@CacheableRule`. Use a class-based rule reference for caching and injectable params.
saying these in an interview costs you the question
- Capturing a script variable inside the rule body instead of injecting it — invisible to the cache, leads to stale results.
- Doing raw network/file I/O in a rule rather than using RepositoryResourceAccessor.