skip to content

What does `@CacheableRule` mean for a Component Metadata Rule, and how do you keep a rule cache-correct while passing external inputs?

level: seniorimportance: should knowfreq 22%

answer

  1. @CacheableRule = output cached cross-build
  2. must be pure function of inputs
  3. no env/file/clock reads
  4. @Inject params + params(...) in cache key
  5. 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 s

Component 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
kotlin
@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

for a junior

Know that rules can be cached and should be simple/deterministic.

for a middle

Explain @CacheableRule means pure-function-of-inputs and that external data goes through injected params.

for a senior

Detail the cache key (metadata + params), RepositoryResourceAccessor, and why env/file reads are forbidden.

for a principal

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.

context