skip to content

What are the directionality and version semantics of a replacedBy rule? Does it pin or introduce the replacement module's version?

level: seniorimportance: should knowfreq 22%

answer

  1. one-way: old -> new, never reciprocal
  2. group:name only, no version in the DSL
  3. does NOT introduce the replacement
  4. does NOT pin the replacement's version
  5. reciprocal A<->B is rejected

basics

~20 s

It is one-directional: old is replaced by new. It never pins or introduces the new module's version — the replacement is only chosen if it is already in the graph, and its version is resolved by Gradle's normal rules.

solid answer

~40 s

A `replacedBy` rule is **directional**: it declares old → new only; it does not also imply new → old. Critically, it carries **no version** for either side — you write `module("g:old")` and `replacedBy("g:new", reason)` with just group:name. Consequences: (1) the rule does not *add* the replacement to your graph; if guava isn't already pulled in by something, google-collections simply stays. (2) It does not *pin* the replacement's version; whatever version of guava the rest of the graph resolves is what you get. So replacement only changes identity/eviction during conflict resolution. If you need to guarantee the new module is present at a specific version, combine the rule with an explicit dependency or use substitution/constraints. Declaring two reciprocal replacements (A replacedBy B and B replacedBy A) is contradictory and rejected.

code

kotlin · 9 lines
kotlin
dependencies {
    modules {
        module("com.foo:old-lib") {
            replacedBy("com.foo:new-lib", "renamed")
        }
    }
    // need it guaranteed present/pinned? declare it yourself:
    implementation("com.foo:new-lib:2.4.0")
}

go deeper

for a junior

Know the DSL takes group:name only and means old becomes new.

for a middle

State that it neither introduces nor pins the replacement, and is one-directional.

for a senior

Explain the conflict-resolution-only nature, the reciprocal-cycle error, and how to combine with constraints/substitution for presence and version control.

for a principal

Set guidance so teams don't mistake replacement for an automatic migration mechanism; pair with explicit upgrades when actually retiring a coordinate.

## Directionality `module("g:old").replacedBy("g:new", "reason")` is a **one-way** statement: *old is superseded by new*. Gradle uses it to evict `old` in favour of `new`. It does not create the reverse relationship. Declaring both directions (A → B and B → A) is a contradiction and Gradle reports an error, because it cannot decide which survives. ## Version semantics — it neither pins nor introduces The DSL takes **group:name only**, never a version. This has two important effects: 1. **No introduction.** The rule will not pull the replacement into your graph. If nothing else depends on `g:new`, then with only `g:old` present there is no conflict, and `g:old` remains. Replacement is purely a *conflict-resolution* rule — it needs both candidates to act. 2. **No version pinning.** When the replacement *is* present, the rule keeps it, but its version is whatever Gradle's ordinary resolution selects from the graph (highest-wins by default, modified by constraints/force/strict declared elsewhere). The replacement rule has zero say over which version of `g:new` you end up with. ```kotlin dependencies { modules { module("com.foo:old-lib") { replacedBy("com.foo:new-lib", "renamed") } } // Replacement does NOT add new-lib. If you want it guaranteed present at a version, // declare it (or a constraint) explicitly: implementation("com.foo:new-lib:2.4.0") } ``` ## When you need more than replacement - **Guarantee presence / version of the new module** → add an explicit dependency or a `constraints { }` entry, or use `dependencySubstitution` which *can* pin a version and introduce the target. - **Express "same library" for dedupe only** → replacement is exactly right and minimal. ## Practical implication Teams sometimes add a replacement rule expecting an automatic migration off a deprecated coordinate, then are surprised the old library is still resolved because nothing pulled the new one in. Remember: replacement reacts to conflicts; it does not initiate migrations.

  • You add replacedBy(old -> new) but the old library is still resolved. Why?
    Nothing in the graph pulls in the new module, so there is no conflict for the rule to resolve. Replacement only evicts old when new is also present; it never introduces the replacement itself.
  • What happens if you declare A replacedBy B and also B replacedBy A?
    Gradle treats it as a contradictory cycle and fails — it cannot determine which module should survive when both claim to replace the other.
  • How do you both dedupe and pin the new module's version?
    Pair the replacement rule with an explicit dependency or a constraint on the new module, or switch to dependencySubstitution, which can both introduce and pin a version.

saying these in an interview costs you the question

  • Believing replacedBy adds the new module or pins its version — it does neither.
  • Assuming the relationship is bidirectional.
  • Putting a version in the module()/replacedBy() coordinates.

context