[JLBP-1]

Minimize dependencies

Use the minimum number of dependencies that is reasonable. Every dependency of a library is a liability of both that library and its consumers.

Adding a dependency for something that is difficult and complicated may be OK, but avoid adding a dependency just to save a few lines of code. Remember you’re also paying the cost of transitive dependencies. Before adding a dependency, consider what that dependency depends on.

Scrutinize all dependency additions. Whenever you add a new dependency, check the transitive dependencies it adds to your classpath. If many new transitive dependencies are pulled in, consider whether the functionality is worth the cost. If a different library solves the same problem but adds fewer dependencies, it might be preferable. Alternatively, if the functionality you need is small, reimplement it in your own library.

Prefer JDK classes where available. For example, XOM and JDOM are very convenient and far easier to use than DOM. However, most uses of these libraries can be satisfied with the org.w3c.dom or other packages bundled with the JDK at some cost in development time.

For any given functionality, pick exactly one library. For example, GSON, Jackson, and javax.json all parse JSON files. If one is already pulled in by another dependency, use that. Otherwise choose one and standardize on it. Do not include more than one in your dependency tree. Do not allow different team members to choose different libraries.

If you can reasonably reimplement functionality instead of adding another dependency, do so. For example, if the only classes you’re using from Guava are Preconditions and Strings, it’s not worth adding a dependency on Guava. You can easily reimplement any method in those classes.

Minimize dependency scope

When you do add a dependency, keep it scoped as narrowly as possible. For example, libraries used only for testing such as JUnit, Mockito, and Truth should have test scope in a Maven build:

  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
  </dependency>

When building with Gradle, these libraries should have testCompile scope:

  dependencies {
    testCompile 'junit:junit:4.12'
  }

Libraries needed at compile time but not at runtime should be marked optional in Maven. For example, dependents of a library that uses AutoValue do not usually need to transitively depend on com.google.auto.value:auto-value-annotations.

  <dependency>
    <groupId>com.google.auto.value</groupId>
    <artifactId>auto-value-annotations</artifactId>
    <version>1.6.6</version>
    <optional>true</optional> <!-- not needed at runtime -->
  </dependency>

In Gradle these libraries should have compileOnly scope:

  dependencies {
    compileOnly 'com.google.auto.value:auto-value-annotations:1.6.6'
  }

The provided scope should be used when the dependency is needed at runtime and compile time. However, the specific jar will be supplied by the environment in which the code runs. For example, Java servlet containers such as Tomcat, Jetty, and App Engine supply javax.servlet:javax.servlet-api.

Separate the tool classpath from the product classpath

The tools we use for Java projects are more often than not themselves written in Java. This includes build systems such as Maven, javac, and Ant; runtime environments such as Tomcat, Hadoop, Beam, and App Engine; and editors such as jEdit, Eclipse, and IntelliJ IDEA. Tools like these should have their own classpaths from which they load their own code.

It is critical not to mix the classpath of the tool with the classpath of the product. Dependencies of the tool should not be dependencies of the product. For example, there’s no reason javax.tools.JavaCompiler should appear in the classpath of an MP3 player simply because the player’s source code was compiled by javac.

Indeed javac does not transmit its own dependencies into the products it builds, but not all tools are this well behaved. Maven annotation processors such as AutoValue and Animal Sniffer are sometimes declared to be dependencies of the product itself in the pom.xml. Since they are only needed at compile time, they should instead be added to the annotation processor path.

When running code from Maven, prefer mvn exec:exec to mvn exec:java. mvn exec:exec uses a completely separate process to run the user’s code while mvn exec:java runs the user’s process in the same virtual machine Maven itself uses.

Modern application servers are reasonably good about separating the code they host from their own. However there have been issues in the past where the boundary was not as aggressively maintained. The jre/lib/ext directory in particular has been a common source of hard to debug dependency issues where a different version of a library was loaded instead of the one the user expected. If you are creating a product that runs user supplied code, implement completely separate classpaths for user code and your product’s code.