Sharing Dependencies and Gradle Plugins between Kotlin/SpringBoot Services

Topher Lamey
Topher Lamey
Share on twitter
Share on facebook
Share on linkedin
Share on reddit
Topher Lamey

Topher Lamey

Share on twitter
Share on facebook
Share on linkedin
Share on reddit

How we handle shared dependencies across our SpringBoot/Kotlin/Gradle projects, allowing us to scale services and team members.

The Backstory

When we were bootstrapping things at StackHawk, we made the decisions to:

  1. Not use a mono-repo
  2. Avoid dependencies across repos based on filesystem layout assumptions

That did require us to handle shared dependencies right up front, but the payoff is that things are scaling nicely as services and team members are added.  For our SpringBoot/Kotlin/Gradle projects, this meant each one had their own plugins and dependencies blocks that look like this:

plugins {
   id("org.springframework.boot") version "2.2.1.RELEASE"
   id("io.spring.dependency-management") version "1.0.8.RELEASE
   [...]
}
 
dependencies {
   implementation("org.springframework.boot:spring-boot-starter-data-jpa")
   implementation("org.springframework.boot:spring-boot-starter-mustache")
  [...]
}

The Drifting Problem

Fast-forward a bit and now that we have more than a few services in their own repos, each with their own plugins and dependencies blocks, we have a drift problem.  We recently wanted all projects to be on a specific version of the SpringBoot plugin, and we realized that updating every project on its own is tedious and error-prone.  Additionally, we noticed that despite our best efforts, the Gradle configs across our relatively young projects had drifted – versions were out of sync, some projects were missing plugins, etc.

This kind of drift can cause subtle issues around expected behavior between projects; if someone in Project A builds functionality around a library function, and then they move to Project B that’s using a different version with different behavior.  Or if someone is using a development tool in Project X that automatically safeguards against a condition, and then they move to Project Y that does not have that tool wired in and they erroneously assume they don’t need to deal with the condition.

Back On Track

Gradle generally wants multi-project builds to be in the same repo, using filesystem-relative things like include, project, or includeFlat to pull common configs together.  But since we don’t have a mono-repo and we do not want to assume multi-project filesystem layouts, we didn’t want to use those Gradle features.

Naturally, Gradle does support sharing plugins and dependencies via published artifacts using the Maven coordinate convention, but it requires a little bit of work.  Turns out, a custom plugin is needed for plugins, and a custom platform is needed for dependencies.  Both of these were easy to build and incorporate into our codebase.

Custom Plugin

Plugins can be shared across projects by writing a custom plugin in its own project.

  1. Set up a custom plugin project using the maven-plugin and java-gradle-plugin plugins in the build.gradle.kts file
  2. Write a custom plugin that extends Plugin<Project>
  3. In the apply method of the custom plugin, use the project.plugins object to apply plugins you want shared via their respective plugin id
  4. Specify the dependent plugin libraries via their classpath Maven coordinates that are referenced by their plugin id in the custom plugin’s build.gradle.kts file.
    1. Note that the plugin ID and the classpath coordinates are different things and that https://plugins.gradle.org/ will give you both pieces of info.
  5. Use the maven-publishing plugin to publish the custom plugin to a Maven repo
  6. In the dependent project, pull in the custom plugin by its plugin id in the plugins block of the build.gradle.kts
  7. In order for the dependent project to find this new plugin via it’s id in the Maven repo, you’ll need to add your repo to the plugin configuration.  We did this in the dependent project’s settings.gradle.kts, in the pluginManagement block.
Example custom plugin build.gradle.kts
plugins {
    `maven-publish`
    `java-gradle-plugin`
    kotlin("jvm") version "1.3.70"
}
 
group = "com.stackhawk.plugins.example"
version = "0.0.1"
java.sourceCompatibility = JavaVersion.VERSION_1_8
 
repositories {
    // Need to include plugin repo
    gradlePluginPortal()
    mavenCentral()
    mavenLocal()
    jcenter()
}
 
dependencies {
    // Plugin classpath artifacts with specific versions
    implementation("org.springframework.boot:spring-boot-gradle-plugin:2.2.5.RELEASE")
    implementation("io.spring.gradle:dependency-management-plugin:1.0.9.RELEASE")
 
    val kotlinPluginVersion = "1.3.70"
    implementation(kotlin("gradle-plugin:${kotlinPluginVersion}"))
    implementation(kotlin("allopen:${kotlinPluginVersion}"))
    implementation(kotlin("noarg:${kotlinPluginVersion}"))
}
 
publishing {
    repositories {
        maven {
            url = uri("http://private-repo/")
        }
    }
}
 
gradlePlugin {
    plugins {
        create("example") {
            // This ID is used by the dependent projects in the plugins block
            id = "com.stackhawk.example"
            implementationClass = "com.stackhawk.gradle.ExamplePlugin"
        }
    }
}
Example custom plugin class
package com.stackhawk.gradle
 
import org.gradle.api.Plugin
import org.gradle.api.Project
 
class ExamplePlugin : Plugin<Project> {
 
    override fun apply(project: Project) {
        // Shared Plugin IDs
        // Also add the Plugin's classpath artifact in build.gradle.kts
        project.plugins.apply("idea")
        project.plugins.apply("jacoco")
        project.plugins.apply("org.springframework.boot")
        project.plugins.apply("org.springframework.boot")
        project.plugins.apply("io.spring.dependency-management")
 
        project.plugins.apply("org.jetbrains.kotlin.jvm")
        project.plugins.apply("org.jetbrains.kotlin.plugin.spring")
        project.plugins.apply("org.jetbrains.kotlin.plugin.jpa")
        project.plugins.apply("org.jetbrains.kotlin.plugin.noarg")
    }
}
Example dependent project build.gradle.kts plugins block
plugins {
 id("com.stackhawk.gradle") version "0.0.4"
}
Example dependent project settings.gradle.kts
rootProject.name = "yarak"
 
pluginManagement {
   // Needed to find our new custom plugin
   repositories {
       gradlePluginPortal()
       mavenLocal()
       maven {
           url = uri("http://private-repo/")
       }
   }
}

Custom Platform

Managing dependencies across projects is done via a platform, which can either be a local project or a Maven BOM (AKA a Parent POM).  Luckily, Gradle provides nice tooling that will auto-generate a BOM for your project via the java-platform plugin.

This requires a separate platform project, as platform projects are disallowed to have actual source code in them:

  1. Set up a platform project using the  using the maven-plugin and java-platform plugins in the build.gradle.kts file
  2. In the dependencies block, use a constraints block to specify dependencies and versions using the api() call for desired Maven coordinates.
  3. Use the maven-publishing plugin to publish the custom plugin to a Maven repo
  4. In the dependent project, specify the new platform in the dependencies block using the api() call with Maven coordinates
Example platform build.gradle.kts
plugins {
    `java-platform`
    `maven-publish`
}
 
group = "com.stackhawk"
version = "0.0.1"
 
dependencies {
    constraints {
        // Platform declares specific versions of libraries used in subprojects
        api("software.amazon.awssdk:aws-sdk-java:2.10.90")
        api("com.ninja-squad:springmockk:2.0.0")
        api("org.apache.httpcomponents:fluent-hc:4.5.12")
    }
}
 
publishing {
    repositories {
        maven {
            url = uri("http://private-repo")
        }
    }
}
Example dependent project build.gradle.kts dependencies block:
dependencies {
  // Use our custom platform via the Maven POM
  api(platform("com.stackhawk:aerie:0.0.1"))
 
  // Dependencies don't need specific versions, the platform fills those in
  implementation("software.amazon.awssdk:aws-sdk-java")
  testImplementation("com.ninja-squad:springmockk")
  testImplementation("org.apache.httpcomponents:fluent-hc")
}

Summary

This solution fits in nicely with our filesystem-layout-agnostic/multi-repo setup and makes our service’s build files more consistent and easier to manage.  Downstream projects simply include the custom plugin and platform and automatically are in line with our dependency versions.  Adding the plugin and platform to our build system has added a little overhead, but the trade-offs make it worthwhile.

KAAKAWW!!! [ kǝn'grats ]

You're on the waitlist!
We can’t wait to get you started with StackHawk. Please complete this 3 minute survey to help us ensure we will be a good fit for your needs.
STACKHAWK - LIMITED EARLY RELEASE
Join the early access program and use StackHawk for free.

KAAKAWW!!! [ kǝn'grats ]

You're signed up for the newsletter!
We’ll keep you up to date on content and other happenings here at StackHawk.