skip to content

What is module replacement in Gradle, and what problem does it solve?

level: middleimportance: must knowfreq 40%

answer

  1. dependencies.modules { module(...).replacedBy(...) }
  2. google-collections -> guava classic case
  3. old and new treated as same logical component
  4. participates in conflict resolution, evicts old
  5. reason string surfaces in dependencyInsight

basics

~20 s

Module replacement tells Gradle that one module is a drop-in successor of another (e.g. google-collections was replaced by guava), so during conflict resolution Gradle treats them as the same logical component and keeps only the replacement.

solid answer

~40 s

Module replacement is a dependency-management rule declared via `dependencies.modules { module('old:lib').replacedBy('new:lib', 'reason') }`. It addresses the case where a library was renamed or migrated to a new coordinate (classic example: `com.google.collections:google-collections` → `com.google.guava:guava`). Without it, both modules land on the classpath because Gradle sees two distinct components, causing duplicate classes and `NoSuchMethodError`-style clashes. The replacement rule makes Gradle recognise the old and new modules as the *same logical thing*, so its normal conflict resolution discards the old one and keeps the replacement. It differs from substitution: replacement is a global, capability-like statement of identity that participates in conflict resolution, not a forced swap of a specific version.

code

kotlin · 7 lines
kotlin
dependencies {
    modules {
        module("com.google.collections:google-collections") {
            replacedBy("com.google.guava:guava", "merged into guava")
        }
    }
}

go deeper

for a junior

Recall that it maps an old renamed library to its new coordinate so you don't get both on the classpath (google-collections → guava).

for a middle

Explain the DSL, that it participates in conflict resolution and evicts the old module, and that the reason string is documentation.

for a senior

Contrast with substitution and forcing; note it only fires when both are present and doesn't pin the new module's version.

for a principal

Discuss when to centralise such rules in a convention/platform plugin so all projects inherit consistent coordinate-rename handling.

## The problem Libraries sometimes change their Maven coordinates — they get renamed, donated to a foundation, or merged. The canonical case is Google Collections (`com.google.collections:google-collections`) which became Guava (`com.google.guava:guava`). Guava *contains* all the old Google Collections classes. If a transitive graph pulls in both, you get **two modules with overlapping classes** on the classpath. The JVM loads whichever appears first, and you typically hit `NoSuchMethodError` or `NoClassDefFoundError` because the old jar shadows newer APIs. Gradle's normal conflict resolution only deduplicates *the same module at different versions*. It has no built-in knowledge that two **different coordinates** are actually the same logical library — so both survive. ## What module replacement does A module-replacement rule states: *"module A has been replaced by module B."* Once declared, when both A and B are present in the resolved graph, Gradle picks B and **evicts A entirely**, as if they were competing versions of one component. ```kotlin dependencies { modules { module("com.google.collections:google-collections") { replacedBy("com.google.guava:guava", "google-collections was merged into guava") } } } ``` The second argument is a human-readable **reason** that surfaces in `dependencyInsight` output, documenting *why* the eviction happened. ## Key semantics - It is declared at the **project level**, inside the `dependencies { modules { ... } }` block (group:name only — no version). - It participates in **conflict resolution**: if only the old module is present and the new one is not, nothing happens — replacement only kicks in when both are in the graph (or when the new one would otherwise be added). - It is **directional**: old → new. It does not force a version of the new module; the new module's own version is still resolved normally. - It is closer to a *capability/identity* statement than to substitution. Substitution (`resolutionStrategy.dependencySubstitution`) unconditionally swaps one selector for another; replacement is a softer rule that only matters during conflict resolution. ## Inspecting the result ```bash ./gradlew dependencyInsight --dependency google-collections ``` shows the old module as evicted with your reason text attached, confirming the rule fired.

  • If only the old module is on the classpath and the new one is absent, does the replacement rule do anything?
    No. Replacement only takes effect during conflict resolution when both modules are present (or the replacement is otherwise pulled in). With only the old module, it stays — the rule does not auto-upgrade you to guava.
  • Does the second argument to replacedBy change behaviour?
    No, it is purely documentation — a reason string that appears in dependencyInsight/resolution output to explain the eviction. Behaviour is identical with any text.

Like a postal forwarding order: mail addressed to the old address (google-collections) is recognised as belonging to the same person now living at the new address (guava), so it isn't delivered twice.

saying these in an interview costs you the question

  • Claiming replacement forces a version of the new module — it does not; it only resolves identity.
  • Saying it auto-migrates code from old to new even when only the old module is present.

context