skip to content

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