In this article you’ll learn the main differences between Gradle api and implementation configurations with a real-world example showing exactly how things work under the covers.

The Java Library Plugin is the recommended plugin to use when building libraries to be consumed by another project. It distinguishes between api and implementation dependencies, offering some key benefits for whoever consumes the library.

UPDATED in July 2021 to use the latest Gradle version.

1. Gradle dependency configuration basics

In Gradle a configuration represents a group of artifacts that you want to use in some way in your build. A dependency, on the other hand, is a reference to a single artifact that is grouped in a configuration.

dependencies {
    implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.2'
}

For example, here we have a dependency on jackson-databind within the implementation configuration. This configuration will get used to generate both compile and runtime classpaths, which you can query using ./gradlew dependencies.

Here’s the compile classpath:

compileClasspath - Compile classpath for source set 'main'.
\--- com.fasterxml.jackson.core:jackson-databind:2.10.2
     +--- com.fasterxml.jackson.core:jackson-annotations:2.10.2
     \--- com.fasterxml.jackson.core:jackson-core:2.10.2

And the runtime classpath:

runtimeClasspath - Runtime classpath of source set 'main'.
\--- com.fasterxml.jackson.core:jackson-databind:2.10.2
     +--- com.fasterxml.jackson.core:jackson-annotations:2.10.2
     \--- com.fasterxml.jackson.core:jackson-core:2.10.2

These classpaths include not only the artifact we depended on but also its transitive dependencies.

In this case the classpaths are exactly the same. This is because the jackson-databind artifact has declared its own dependencies as compile scope dependencies, which you can see in the Maven Central repository:

And according to the Maven docs, anything in the compile scope is also included in the runtime scope

Compile dependencies are available in all classpaths of a project.

But what if we wanted to have more fine-grained control over the compile and runtime classpaths?

Wouldn’t it be nice if only artifacts that really need to be on the compile classpath are added to it, and all the others go on the runtime classpath?

Can Gradle provide a way to do this? 😉

2. Benefits of fine-grained classpath control

So let’s imagine a scenario where you have an Application that depends on Library A which has two transitive dependencies, Library B & Library C:

Some additional information:

  1. Application uses classes from Library A

  2. Library A uses classes from Library B and Library C

  3. Library A exposes Library B on its interface (e.g. one of it’s classes could return a type defined in Library B)

  4. Library C is only used internally of Library A (e.g. inside methods)

Application binary interface (ABI)

What’s alluded to in points 3 & 4 above is what’s known as the library binary interface or application binary interface (ABI). Some types that fall into the ABI include:

  • public method parameters

  • return types

  • types used in parent classes or interfaces

Types that don’t fall into the ABI include:

  • types used in method bodies

  • types defined in private method declarations

The important thing to remember about the ABI is that any types used within it need to be declared on the compile classpath. With this information then, we can think about how we’d build up the compile and runtime classpaths for Application:

Compile classpath
  • Library A, as we interact directly with this

  • Library B, as Library A exposes this on its interface that we interact with

Runtime classpath
  • Library A

  • Library B

  • Library C, as this is used internally in Library A

If we had the ability to build up these classpaths selectively like this then, we’d benefit from the following:

  1. cleaner classpaths

  2. won’t accidentally use a library that we haven’t depended on explicitly e.g. can’t use Library C in Application as it’s not on the compile classpath

  3. faster compilation due to our cleaner classpath

  4. less recompilation as when artifacts on the runtime classpath change we don’t need to recompile

3. The Java Library Gradle plugin makes this possible

The Java Library Gradle plugin makes this fine-grained classpath control possible. It’s up to you as the creator of a library to define which dependencies should be included in the runtime or compile classpaths of whatever application is consuming this library.

We achieve this with the following dependency configurations:

  • api - dependencies in the api configuration are part of the ABI of the library we’re writing and therefore should appear on the compile and runtime classpaths

  • implementation - dependencies in the implementation configuration aren’t part of the ABI of the library we’re writing. They will appear only on the runtime classpath.

Going back to our example from before then, if we were writing Library A our dependencies in our build.gradle would look something like this:

dependencies { api ’library-b' implementation ’library-c' }

This enables the Java Library plugin to generate the artifact with the relevant information so that the consumer can construct the classpath correctly.

4. Real world example

Enough of this Library A-Library B stuff then! Let’s get into a real life example and see what’s going on under the hood.

We’re going to generate 2 projects:

  1. a library (called gradle-java-library-plugin-library in GitHub): this is the library we’ll build with the Java Library plugin

  2. a consumer (called gradle-java-library-plugin-consumer in GitHub): this is the application that will depend on the library

A library using the Java Library plugin

Our build.gradle looks like this:

plugins {
    id 'java-library'
    id 'maven-publish'
}

group = 'com.tomgregory'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    implementation group: 'com.google.http-client', name: 'google-http-client', version: '1.34.2'
    api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.10.2'
}

publishing {
    publications {
        maven(MavenPublication) {
            from components.java
        }
    }
}
  • we’re applying the Java Library plugin as already discussed
  • we’re also applying the Maven Publish plugin as we’ll need to publish to Maven local so that our consumer can pull the artifact. A publishing block is included at the end to set this up properly.
  • we have an implementation dependency on google-http-client. Remember this means our consumer will have only a runtime dependency on this artifact, so it cannot appear in the ABI of this library.
  • we have an api dependency on com.fasterxml.jackson.core. This means our consumer will have both a compile and runtime dependency on this artifact. It can therefore appear in the ABI of this library.

Our library has just one class, AwesomeService, which we’ll put in a package com.tomgregory:

package com.tomgregory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.javanet.NetHttpTransport;

import java.io.IOException;

public class AwesomeService {
    private ObjectMapper objectMapper = new ObjectMapper();

    public JsonNode getWebPage() throws IOException {
        HttpRequestFactory requestFactory = new NetHttpTransport().createRequestFactory();
        HttpRequest request = requestFactory.buildGetRequest(new GenericUrl("http://get-simple.info/api/start/"));
        String response =  request.execute().parseAsString();

        return objectMapper.readValue(response, JsonNode.class);
    }
}

This class exposes a method that hits a REST API and returns the result. Note that:

  • the method returns JsonNode which is part of the com.fasterxml.jackson.core dependency. This return type forms part of this class’s ABI.

  • the method body only uses the google-http-client library to make an HTTP request. google-http-client is therefore not part of the ABI of this class.

Publishing our library to Maven local

Let’s now run ./gradlew publishToMavenLocal and see what we end up with:

$ ./gradlew publishToMavenLocal

BUILD SUCCESSFUL in 4s
5 actionable tasks: 5 executed

Navigating to my ~/.m2/repository/com/tomgregory directory I can see we’ve now got a gradle-java-library-plugin-library directory. Within that we have another directory for our 0.0.1-SNAPSHOT version:

Let’s then take a look inside the pom file, which any consumers of this library can use to determine transitive dependencies:

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <!-- This module was also published with a richer model, Gradle metadata,  -->
  <!-- which should be used instead. Do not delete the following line which  -->
  <!-- is to indicate to Gradle or any Gradle module metadata file consumer  -->
  <!-- that they should prefer consuming it instead. -->
  <!-- do_not_remove: published-with-gradle-metadata -->
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.tomgregory</groupId>
  <artifactId>gradle-java-library-plugin-library</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <dependencies>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.10.2</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>com.google.http-client</groupId>
      <artifactId>google-http-client</artifactId>
      <version>1.34.2</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

You can see here that:

  • jackson-databind is a compile time scoped dependency

  • google-http-client is a runtime scoped dependency

Awesome Gradle, you got it right! ✅ Therefore any consumers of this artifact should honour the scopes of these transitive dependencies.

Consuming our library

The last step here is to consume this library within a Gradle built application and ensure the classpaths are setup according to the scopes defined in the library’s pom file. We’ll create a project called gradle-java-library-plugin-consumer.

The build.gradle file for the consumer should look like this:

plugins {
    id 'java'
}

group 'com.tomgregory'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
    mavenLocal()
}

dependencies {
    implementation group: 'com.tomgregory', name: 'gradle-java-library-plugin-library', version: '0.0.1-SNAPSHOT'
}
  • we’re applying the Java plugin rather than the Java Library plugin, as this is a plain old Java project
  • we’re adding mavenLocal() and mavenCentral() repositories, so we can pull the library we just published locally as well as its transitive dependencies
  • we have an implementation dependency on our library

And just for fun lets add a Java class DoStuff to the com.tomgregory package, to make sure this all works:

package com.tomgregory;

import java.io.IOException;

public class DoStuff {
    public static void main(String[] args) throws IOException {
        AwesomeService awesomeService = new AwesomeService();

        System.out.println(awesomeService.getWebPage().get("status"));
    }
}

Here we’re instantiating the service provided by our library and calling getWebPage. We’re also calling the get method of JsonNode, provided by the jackson-databind artifact available on the compile classpath. This method drills down into the returned JSON object to get the value of the status field.

Let’s run the main method:

16:19:48: Executing task 'DoStuff.main()'...

> Task :compileJava
> Task :processResources NO-SOURCE
> Task :classes

> Task :DoStuff.main()
"9"

BUILD SUCCESSFUL in 2s
2 actionable tasks: 2 executed
16:19:51: Task execution finished 'DoStuff.main()'.

Inspecting the classpath

Everything seems to be working in our application as expected, but if you’re anything like me you’ll want to see under the covers to verify this. Fortunately Gradle provides the dependencies task which shows us both the compile and runtime classpaths.

To get the compile classpath run ./gradlew dependencies --configuration compileClasspath

compileClasspath - Compile classpath for source set 'main'.
\--- com.tomgregory:gradle-java-library-plugin-library:0.0.1-SNAPSHOT
     \--- com.fasterxml.jackson.core:jackson-databind:2.10.2
          +--- com.fasterxml.jackson.core:jackson-annotations:2.10.2
          \--- com.fasterxml.jackson.core:jackson-core:2.10.2

And for the runtime classpath run ./gradlew dependencies --configuration runtimeClasspath

runtimeClasspath - Runtime classpath of source set 'main'.
\--- com.tomgregory:gradle-java-library-plugin-library:0.0.1-SNAPSHOT
     +--- com.google.http-client:google-http-client:1.34.2
     |    +--- org.apache.httpcomponents:httpclient:4.5.11
     |    |    +--- org.apache.httpcomponents:httpcore:4.4.13
     |    |    +--- commons-logging:commons-logging:1.2
     |    |    \--- commons-codec:commons-codec:1.11
     |    +--- org.apache.httpcomponents:httpcore:4.4.13
     |    +--- com.google.code.findbugs:jsr305:3.0.2
     |    +--- com.google.guava:guava:28.2-android
     |    |    +--- com.google.guava:failureaccess:1.0.1
     |    |    +--- com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava
     |    |    +--- com.google.code.findbugs:jsr305:3.0.2
     |    |    +--- org.checkerframework:checker-compat-qual:2.5.5
     |    |    +--- com.google.errorprone:error_prone_annotations:2.3.4
     |    |    \--- com.google.j2objc:j2objc-annotations:1.3
     |    +--- com.google.j2objc:j2objc-annotations:1.3
     |    +--- io.opencensus:opencensus-api:0.24.0
     |    |    \--- io.grpc:grpc-context:1.22.1
     |    \--- io.opencensus:opencensus-contrib-http-util:0.24.0
     |         +--- io.opencensus:opencensus-api:0.24.0 (*)
     |         \--- com.google.guava:guava:26.0-android -> 28.2-android (*)
     \--- com.fasterxml.jackson.core:jackson-databind:2.10.2
          +--- com.fasterxml.jackson.core:jackson-annotations:2.10.2
          \--- com.fasterxml.jackson.core:jackson-core:2.10.2

We can see here that the compile classpath as expected has our library and jackson-databind only. The runtime classpath, on the other hand, has google-http-client and all of its transitive dependencies.

5. Final thoughts

If you’re still with me at this point, hopefully you can see the power of using api and implementation dependencies in your libraries.

It might not make that much difference for a small project, but once you start creating multiple libraries consumed by multiple applications, you’ll benefit from following the best practice described above.

6. Resources

GITHUB REPOSITORIES Grab the library code Get the consumer code

FURTHER READING Check out these Gradle docs on the Java Library plugin

Video

Check out the accompanying video from the Tom Gregory Tech YouTube channel.