Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

Understanding Configuration Inheritance with `extendsFrom`

Recommended resources

Welcome to our next session. In our last lesson, we categorized Gradle configurations into three distinct roles: declarable, resolvable, and consumable. This provided a conceptual framework for understanding how dependencies are declared, used internally, and published externally.

Today, we will connect those concepts by examining the mechanism that links these configurations together: inheritance. The goal of this lesson is to learn how to model configuration relationships using the extendsFrom keyword. This is the "glue" that allows dependencies declared in one configuration to flow into others, forming the dependency graphs that power your build.

The extendsFrom Keyword: Creating a Hierarchy

At its core, Gradle's configuration model is a directed acyclic graph where configurations are nodes and extendsFrom declarations are edges. When one configuration extends another, it inherits its dependencies, dependency constraints, and exclude rules. This is a powerful feature for reducing duplication and creating logical dependency scopes.

Let's look at the diagram of the standard Java plugin configurations again. The arrows imply an inheritance relationship. For example, dependencies added to implementation are available to the runtimeClasspath. This connection is made using extendsFrom.

This diagram illustrates the flow of dependencies in a standard Java project. Each dashed line represents an `extendsFrom` relationship, where a configuration inherits the dependencies of another.

It is crucial to understand what is and is not inherited:

  • Inherited:
    • Dependencies
    • Dependency constraints
    • Exclude rules
  • Not Inherited:
    • Attributes (used for variant-aware resolution, which we'll cover later)
    • Role-defining flags (canBeDeclared, canBeResolved, canBeConsumed)

This distinction is important. Extending a configuration gives you its dependencies, but it doesn't mean the new configuration must serve the same role. For instance, a resolvable configuration like runtimeClasspath commonly extends a declarable one like implementation.

The official Gradle documentation provides a concise section on this topic.

Creating Dependency Configurations - Gradle User Manual

This section from the Gradle User Manual formally defines configuration inheritance and details what properties are inherited.

Please read the section titled Configuration inheritance. Pay close attention to the list of what is and is not inherited. The smokeTest example is a classic use case: creating a specialized test scope that reuses all the dependencies from the main test scope (testImplementation) while adding its own.


Applying Inheritance to the Three Roles

The primary pattern you'll see and use is having resolvable and consumable configurations extend declarable ones. This creates a clear flow:

  1. You declare dependencies on a "bucket" configuration (e.g., implementation).
  2. An internal, resolvable configuration (e.g., runtimeClasspath) extends it to create a classpath for a task.
  3. An external, consumable configuration (e.g., runtimeElements) also extends it to publish those dependencies to other projects.

The blog post we reviewed in the previous lesson provides excellent code examples demonstrating this exact pattern, both with the older, manual approach and the modern, role-aware APIs.

Configuration roles and the blogging-industrial complex

This article shows the practical application of extendsFrom to connect configurations of different roles.

First, quickly review the "old way" code block under the first code sample. Notice how both runtimeClasspath and runtimeElements explicitly use extendsFrom(configurations["implementation"]). Next, look at the "new way" code block just below it, which uses the modern API: new methods. The pattern is identical. Finally, in the plugin example, examine how the author sets up the custom configurations under the heading Setting up our configurations. This is a perfect, real-world example of creating declarable, resolvable, and consumable configurations that are linked together with extendsFrom.

This pattern of resolvable { extendsFrom(declarable) } and consumable { extendsFrom(declarable) } is fundamental to modern Gradle plugin development. It ensures a clear separation of concerns while maintaining the correct flow of dependencies.


An Example in Practice

Let's solidify this with the final example from the Gradle documentation, which combines the modern role-aware API with the extendsFrom keyword. It cleanly demonstrates how a resolvable classpath is built upon a declarable dependency scope.

Creating Dependency Configurations - Gradle User Manual

This final example puts the pieces together, showing how to create and extend configurations for dependency resolution.

Read the section titled Dependency resolution and study the accompanying code snippets for both Kotlin and Groovy DSLs. This shows precisely how a resolvable runtimeClasspath is defined to inherit dependencies from a declarable implementation configuration.

This example, though simple, is the blueprint for how most dependency graphs are constructed in Gradle. By defining these small, interconnected pieces, you can build up complex and highly specific dependency scopes without chaos.


Conclusion

In this lesson, we explored the mechanics of configuration inheritance, the cornerstone of structuring dependency graphs in Gradle.

Here are the key takeaways:

  • extendsFrom is the keyword used to create an inheritance relationship between configurations.
  • The extending configuration inherits dependencies, constraints, and exclude rules, but not attributes or role definitions.
  • This mechanism is the "glue" that connects the three configuration roles: resolvable and consumable configurations typically extend declarable ones to receive their sets of dependencies.
  • This pattern allows for a clean separation of concerns, where dependencies are declared once in a "bucket" and then channeled into different classpaths or publications as needed.

In our next lesson, we will put this knowledge to use by focusing on how to create your own custom configurations to manage very specific dependency scopes within your project. You now have the tools to not only create them but also integrate them effectively into the existing configuration hierarchy.