How to use Gradle api vs. implementation dependencies with the Java Library plugin

Gradle api vs implementation dependencies

Last Updated on July 3, 2020

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 that will be consumed by another project. It offers the ability to distinguish between api and implementation dependencies, offering some key benefits for whoever consumes the library.

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 I 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.

Info: at the time of writing the compile configuration is in a deprecated state, and should be replaced with implementation

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, we can see that 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 it’s 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 represents our library that we’ll be building making use of the Java Library plugin
  2. a consumer (called gradle-java-library-plugin-consumer in GitHub): this represents our application that’s going to 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 that this means there 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:

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:

Info: the .pom file is an xml file containing metadata about the artifact, popularised by the Maven build tool. This metadata format is still used, even when using different build tools such as Gradle.

<?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() as well as mavenCentral() repositories, so we can pull the library which we just published locally
  • 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:

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
If you prefer to learn in video format, check out this accompanying video to this post on the Tom Gregory Tech YouTube channel.

How to use Gradle api vs. implementation dependencies with the Java Library plugin

7 thoughts on “How to use Gradle api vs. implementation dependencies with the Java Library plugin

  1. Hi Tom,

    I am new to Gradle and i came across your site while searching for Gradle tutorials.
    I find your tutorials to be very helpful for a new beginner in Gradle.

    i am using Gradle 6.1 and I am writing small application to understand the concepts of multi project Application and Java-Library plugin of Gradle.

    I have some doubts regarding multi-project application and “api” configuration of Java-Library plugin in gradle.

    I have posted my doubt in Stackoverflow and below is the link of my Stackoverflow post.

    https://stackoverflow.com/q/61429559/1427169

    Can you please help me to clear my doubts.

    Regards

    1. Hi K. Verma. Thanks for your comment. I can see that your StackOverflow question has already been answered.

      It’s an interesting point though. It seems that IntelliJ cannot recognise the ‘api’ method when using the approach of configuring subprojects from the parent e.g.

      project(‘:SubProject-2’) {
      apply plugin: ‘java-library’

      dependencies {
      api ‘org.apache.commons:commons-math3:3.2’
      }
      }

      Although we can see that the above code does build in Gradle, IntelliJ cannot recognise ‘api’ and shows it as greyed out in the editor. I’m going to assume this is due to how IntelliJ is managing the application of the ‘java-library’ plugin in this case.

  2. Hi Tom Gregory,
    Awesome blog with realtime example answered most of my questions related to `ap` and `implementation`.

    I think there is typo in below line.
    `Library C, as this is used internally in Library B` ==> `Library C, as this is used internally in Library A`

    1. Hi Karthik. Glad you’re finding the blog helpful. Thanks so much for highlighting my mistake, which I’ve now corrected.

      Looking forward to your feedback on future posts.

      Tom

  3. Hi, thank you for the helpful blog post. I cloned the library project and ran ./gradlew dependencies, but i see both libs listed under compileClasspath: https://pastebin.com/ZJnBDZhe which is different from the instructions in the blog post. Can you please tell me what am I doing wrong?

  4. Nevermind, I just realized I was supposed to run the dependencies task on the consumer, not on the library xD

Leave a Reply

Your email address will not be published. Required fields are marked *

Scroll to top

To keep up to date with all things to do with scaling developer productivity, subscribe to my monthly newsletter!