skip to content

Module Replacement

Declaring that one module replaces another so conflict resolution treats the old and new coordinates as the same thing. Interviewers reach for the google-collections-to-guava case as the canonical example.

on this pageshow

questions

5

Walk through the classic google-collections to guava problem and how a module replacement rule fixes it.

level: middleimportance: must knowfreq 38%

answer

  1. guava = superset of google-collections, same package names
  2. different coordinates -> Gradle keeps both
  3. duplicate classes -> NoSuchMethodError at runtime
  4. replacedBy evicts google-collections
  5. verify with dependencyInsight

basics

~10 s

Guava absorbed all of google-collections' classes. If both end up on the classpath you get duplicate classes and NoSuchMethodError. Declaring google-collections replacedBy guava makes Gradle evict the old one, leaving only guava.

solid answer

~40 s

Google Collections (`com.google.collections:google-collections`) was the predecessor of Guava (`com.google.guava:guava`); Guava contains a superset of its classes under the same package names. Some older transitive dependencies still declare google-collections. When your graph also pulls in guava, both jars are placed on the compile/runtime classpath. Because they share class names, the classloader picks one arbitrarily; you typically get `NoSuchMethodError` at runtime when newer Guava methods are missing from the old jar that won. Gradle won't dedupe automatically because they are *different* coordinates. The fix is a module-replacement rule: `dependencies { modules { module("com.google.collections:google-collections") { replacedBy("com.google.guava:guava", "merged into guava") } } }`. Now Gradle treats them as one logical component and evicts google-collections during conflict resolution, leaving a single clean guava on the classpath. Verify with `dependencyInsight`.

code

kotlin · 8 lines
kotlin
dependencies {
    modules {
        module("com.google.collections:google-collections") {
            replacedBy("com.google.guava:guava", "merged into guava")
        }
    }
}
// ./gradlew dependencyInsight --dependency google-collections

go deeper

for a junior

Recall the symptom (both on classpath, NoSuchMethodError) and that the replacement rule removes the old one.

for a middle

Explain why Gradle keeps both (different coordinates) and write the replacedBy rule plus how to verify it.

for a senior

Mention capability metadata in modern Guava that can handle this without the rule, and contrast with the portable replacement approach.

for a principal

Decide whether to bake such rules into a shared platform plugin so every team inherits consistent handling of legacy renames.

## Background Guava is the successor of the older **Google Collections** library. When Google rebranded and expanded it, the classes from `com.google.collections:google-collections` were carried forward into `com.google.guava:guava` under the *same fully-qualified names* (e.g. `com.google.common.collect.ImmutableList`). ## Why a conflict arises Gradle's automatic version conflict resolution operates on a single `group:name` identity. `com.google.collections:google-collections` and `com.google.guava:guava` are **two different identities**, so Gradle keeps both — it has no way to know they are the same library. A legacy transitive dependency may still request google-collections while your app (or another dep) requests guava. Result: two jars with overlapping classes on the classpath. The JVM loads classes from whichever jar comes first in classpath order. If the old google-collections jar wins, code compiled against modern Guava APIs throws `NoSuchMethodError`/`NoClassDefFoundError` at runtime. ## The fix ```kotlin dependencies { modules { module("com.google.collections:google-collections") { replacedBy("com.google.guava:guava", "google-collections was merged into guava") } } } ``` This tells Gradle: these two coordinates are the same logical component, and guava is the replacement. During conflict resolution, google-collections is **evicted**, and only guava remains. Because guava is a superset, all required classes are present and at the right versions. ## Verifying ```bash ./gradlew dependencyInsight --dependency google-collections ``` The report shows google-collections as not selected, annotated with your reason. `./gradlew dependencies` will show only guava on the resolved classpath. ## Note on modern publishing Guava itself publishes Gradle Module Metadata that already declares a `com.google.collections:google-collections` **capability**, so on recent Gradle versions this specific clash is often handled by capability conflict resolution without you writing the rule. The replacement rule remains the portable, explicit way to handle *any* renamed library where such metadata is absent.

  • Why doesn't Gradle deduplicate these two automatically by default?
    Its built-in conflict resolution dedupes only the same group:name at different versions. google-collections and guava are different coordinates, so without a replacement rule (or capability metadata) Gradle sees two unrelated components and keeps both.
  • How would you confirm the rule actually evicted the old module?
    Run `./gradlew dependencyInsight --dependency google-collections`; the report lists it as not selected with your reason text, and `./gradlew dependencies` shows only guava on the classpath.
  • Is the explicit rule still always necessary on modern Gradle?
    Not always — recent Guava metadata declares a google-collections capability, so capability conflict resolution can handle this exact case. The replacement rule is still the portable approach for arbitrary renamed libraries lacking such metadata.

saying these in an interview costs you the question

  • Saying Gradle auto-resolves it because the class names match — it resolves by coordinate, not by class content.
  • Confusing the symptom (NoSuchMethodError) with a version conflict of a single module.

context

open as a page

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

level: middleimportance: must knowfreq 40%

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.

open as a page

Where and how do you declare a module replacement rule, and how can you apply it consistently across many subprojects?

level: middleimportance: should knowfreq 25%

basics

~20 s

Declare it in the dependencies block: dependencies { modules { module('g:old') { replacedBy('g:new', 'reason') } } }. To apply across subprojects, put it in a convention plugin or a shared script applied to each project.

open as a page

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%

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.

open as a page

How does module replacement differ from dependency substitution, and when would you choose one over the other?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Substitution unconditionally swaps one selector for another module/project. Module replacement only states that two coordinates are the same logical library, so the old one is evicted during conflict resolution if both appear. Use replacement for renamed libraries, substitution for forced swaps.

open as a page