The Backstory
When we were bootstrapping things at StackHawk, we made the decisions to:
Not use a mono-repo
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 ownplugins
anddependencies
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 ownplugins
anddependencies
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 likeinclude
,project
, orincludeFlat
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.
Set up a custom plugin project using themaven-plugin and java-gradle-plugin
plugins in thebuild.gradle.kts fileWrite a custom plugin that extendsPlugin<Project>
- In theapply method of the custom plugin, use theproject.plugins object toapply plugins you want shared via their respective plugin id
Specify the dependent plugin libraries via their classpath Maven coordinates that are referenced by their plugin id in the custom plugin’sbuild.gradle.kts
file.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.
- Use themaven-publishing plugin to publish the custom plugin to a Maven repo
- In the dependent project, pull in the custom plugin by its plugin id in theplugins block of thebuild.gradle.kts
- 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’ssettings.gradle.kts, in thepluginManagement block.
Example custom pluginbuild.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 projectbuild.gradle.kts plugins block
plugins {
id("com.stackhawk.gradle") version "0.0.4"
}
Example dependent projectsettings.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 thejava-platform
plugin.
This requires a separate platform project, as platform projects are disallowed to have actual source code in them:
- Set up a platform project using the using themaven-plugin andjava-platform plugins in thebuild.gradle.kts file
- In thedependencies block, use aconstraints block to specify dependencies and versions using theapi()call for desired Maven coordinates.
- Use themaven-publishing plugin to publish the custom plugin to a Maven repo
- In the dependent project, specify the newplatform in the dependencies block using theapi() call with Maven coordinates
Example platformbuild.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 projectbuild.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.