Maven Scopes and Gradle Configurations Explained

  • July 24, 2019
Table Of Contents

One of the key features of a build tool for Java is dependency management. We declare that we want to use a certain third-party library in our own project and the build tool takes care of downloading it and adding it to the classpath at the right times in the build lifecycle. One of the key features of a build tool for Java is dependency management. We declare that we want to use a certain third-party library in our own project and the build tool takes care of downloading it and adding it to the classpath at the right times in the build lifecycle.

Maven has been around as a build tool for a long time. It’s stable and still well liked in the Java community.

Gradle has emerged as an alternative to Maven quite some time ago, heavily relying on Maven dependency infrastructure, but providing a more flexible way to declare dependencies.

Whether you’re moving from Maven to Gradle or you’re just interested in the different ways of declaring dependencies in Maven or Gradle, this article will give an overview.

What’s a Scope / Configuration?

A Maven pom.xml file or a Gradle build.gradle file specifies the steps necessary to create a software artifact from our source code. This artifact can be a JAR file or a WAR file, for instance.

In most non-trivial projects, we rely on third-party libraries and frameworks. So, another task of build tools is to manage the dependencies to those third-party libraries and frameworks.

Say we want to use the SLF4J logging library in our code. In a Maven pom.xml file, we would declare the following dependency:

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.26</version>
    <scope>compile</scope>
</dependency>

In a Gradle build.gradle file, the same dependency would look like this:

implementation 'org.slf4j:slf4j-api:1.7.26'

Both Maven and Gradle allow to define different groups of dependencies. These dependency groups are called “scopes” in Maven and “configurations” in Gradle.

Each of those dependency groups has different characteristics and answers the following questions differently:

  • In which steps of the build lifecycle will the dependency be made available? Will it be available at compile time? At runtime? At compile and runtime of tests?
  • Is the dependency transitive? Will it be exposed to consumers of our own project, so that they can use it, too? If so, will it leak into the consumers' compile time and / or the consumers' runtime?
  • Is the dependency included in the final build artifact? Will the WAR or JAR file of our own project include the JAR file of the dependency?

In the above example, we added the SLF4J dependency to the Maven compile scope and the Gradle implementation configuration, which can be considered the defaults for Maven and Gradle, respectively.

Let’s look at the semantics of all those scopes and configurations.

Maven Scopes

Maven provides 6 scopes for Java projects.

We’re not going to look at the system and import scopes, however, since they are rather exotic.

compile

The compile scope is the default scope. We can use it when we have no special requirements for declaring a certain dependency.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • compile time
  • runtime
  • test compile time
  • test runtime
yes yes yes

Note that the compile scope leaks into the compile time, thus promoting dependency pollution.

provided

We can use the provided scope to declare a dependency that will not be included in the final build artifact.

If we rely on the Servlet API in our project, for instance, and we deploy to an application server that already provides the Servlet API, then we would add the dependency to the provided scope.

| When available? | Leaks into consumers' compile time? | Leaks into consumers' runtime? | Included in Artifact? | | ———————————————————————————- | ———— | ——————— | |

  • compile time
  • runtime
  • test compile time
  • test runtime
| no | no | no |

runtime

We use the runtime scope for dependencies that are not needed at compile time, like when we’re compiling against an API and only need the implementation of that API at runtime.

An example is SLF4J where we include slf4j-api to the compile scope and an implementation of that API (like slf4j-log4j12 or logback-classic) to the runtime scope.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • runtime
  • test runtime
no yes yes

test

We can use the test scope for dependencies that are only needed in tests and that should not be available in production code.

Examples dependencies for this scope are testing frameworks like JUnit, Mockito, or AssertJ.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • test compile time
  • test runtime
no no no

Gradle Configurations

Gradle has a more diverse set of configurations. This is the result of Gradle being younger and more actively developed, and thus able to adapt to more use cases.

Let’s look at the standard configurations of Gradle’s Java Library Plugin. Note that we have to declare the plugin in the build script to get access to the configurations:

plugins {
    id 'java-library'
}

implementation

The implementation configuration should be considered the default. We use it to declare dependencies that we don’t want to expose to our consumers' compile time.

This configuration was introduced to replace the deprecated compile configuration to avoid polluting the consumer’s compile time with dependencies we actually don’t want to expose.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • compile time
  • runtime
  • test compile time
  • test runtime
no yes yes

api

We use the api configuration do declare dependencies that are part of our API, i.e. for dependencies that we explicitly want to expose to our consumers.

This is the only standard configuration that exposes dependencies to the consumers' compile time.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • compile time
  • runtime
  • test compile time
  • test runtime
yes yes yes

compileOnly

The compileOnly configuration allows us to declare dependencies that should only be available at compile time, but are not needed at runtime.

An example use case for this configuration is an annotation processor like Lombok, which modifies the bytecode at compile time. After compilation it’s not needed anymore, so the dependency is not available at runtime.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • compile time
no no no

runtimeOnly

The runtimeOnly configuration allows us to declare dependencies that are not needed at compile time, but will be available at runtime, similar to Maven’s runtime scope.

An example is again SLF4J where we include slf4j-api to the implementation configuration and an implementation of that API (like slf4j-log4j12 or logback-classic) to the runtimeOnly configuration.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • runtime
no yes yes

testImplementation

Similar to implementation, but dependencies declared with testImplementation are only available during compilation and runtime of tests.

We can use it for declaring dependencies to testing frameworks like JUnit or Mockito that we only need in tests and that should not be available in the production code.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • test compile time
  • test runtime
no no no

testCompileOnly

Similar to compileOnly, but dependencies declared with testCompileOnly are only available during compilation of tests and not at runtime.

I can’t think of a specific example, but there may be some annotation processors similar to Lombok that are only relevant for tests.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • test compile time
no no no

testRuntimeOnly

Similar to runtimeOnly, but dependencies declared with testRuntimeOnly are only available during runtime of tests and not at compile time.

An example would be declaring a dependency to the JUnit Jupiter Engine, which runs our unit tests, but which we don’t compile against.

When available? Leaks into consumers' compile time? Leaks into consumers' runtime? Included in Artifact?
  • test runtime
no no no

Combining Gradle Configurations

Since the Gradle configurations are very specific, sometimes we might want to combine their features. In this case, we can declare a dependency with more than one configuration. For example, if we want a compileOnly dependency to also be available at test compile time, we additionally declare it to the testCompileOnly configuration:

dependencies {
  compileOnly 'org.projectlombok:lombok:1.18.8'
  testCompileOnly 'org.projectlombok:lombok:1.18.8'
}

To remove the duplicate declaration, we could also tell Gradle that we want the testCompileOnly configuration to include everything from the compileOnly configuration:

configurations {
  testCompileOnly.extendsFrom compileOnly
}

dependencies {
  compileOnly 'org.projectlombok:lombok:1.18.8'
}

Do this with care, however, since we’re losing flexibility in declaring dependencies every time we’re combining two configurations this way.

Maven Scopes vs. Gradle Configurations

Maven scopes don’t translate perfectly to Gradle configurations because Gradle configurations are more granular. However, here’s a table that translates between Maven scopes and Gradle configurations with a few notes about differences:

Maven Scope Equivalent Gradle Configuration
compile api if the dependency should be exposed to consumers, implementation if not
provided compileOnly (note that the provided Maven scope is also available at runtime while the compileOnly Gradle configuration is not)
runtime runtimeOnly
test testImplementation

Conclusion

Gradle, being the younger build tool, provides a lot more flexibility in declaring dependencies. We have finer control about whether dependencies are available in tests, at runtime or at compile time.

Furthermore, with the api and implementation configurations, Gradle allows us to explicitly specify which dependencies we want to expose to our consumers, reducing dependency pollution to the consumers.

Written By:

Tom Hombergs

Written By:

Tom Hombergs

As a professional software engineer, consultant, architect, general problem solver, I've been practicing the software craft for more than fifteen years and I'm still learning something new every day. I love sharing the things I learned, so you (and future me) can get a head start. That's why I founded reflectoring.io.

Recent Posts

Guide to JUnit 5 Functional Interfaces

In this article, we will get familiar with JUnit 5 functional interfaces. JUnit 5 significantly advanced from its predecessors. Features like functional interfaces can greatly simplify our work once we grasp their functionality.

Read more

Getting Started with Spring Security and JWT

Spring Security provides a comprehensive set of security features for Java applications, covering authentication, authorization, session management, and protection against common security threats such as CSRF (Cross-Site Request Forgery).

Read more

Creating and Publishing an NPM Package with Automated Versioning and Deployment

In this step-by-step guide, we’ll create, publish, and manage an NPM package using TypeScript for better code readability and scalability. We’ll write test cases with Jest and automate our NPM package versioning and publishing process using Changesets and GitHub Actions.

Read more