Adding third-party libraries, transitive dependencies and how to control them, speeding up Gradle builds, unit testing with JUnit 5, the build cache, publishing to a remote repository, automated code-quality checks, one version across many modules, writing a Gradle plugin, and why the Gradle Wrapper matters.
Published September 25, 2026
The questions here are about making Gradle fast, consistent and shareable across many modules and teams. The key modern answers: version catalogs and platforms for consistency, convention plugins for sharing, and build cache plus configuration cache for speed.
Short answer:
implementation, api, runtimeOnly)../gradlew dependencies --configuration runtimeClasspath) for conflicts, and align with BOMs if needed.# gradle/libs.versions.toml
[versions]
resilience4j = "2.2.0"
[libraries]
resilience4j-spring-boot3 = { module = "io.github.resilience4j:resilience4j-spring-boot3", version.ref = "resilience4j" }
Short answer: Gradle resolves the full graph, and when several versions of a module appear, it chooses the highest version by default (unlike Maven's nearest-wins). Gradle Module Metadata adds variants and capabilities, so the right flavour of a dependency is chosen. You can customise this with:
resolutionStrategy: force, failOnVersionConflict(), dependency substitution, cache policies.dependencies {
implementation("com.example:sdk:3.1.0") {
exclude(group = "commons-logging", module = "commons-logging")
}
constraints {
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") { because("CVE fix, aligned with Boot") }
}
}
Short answer: Measure first with a build scan (--scan) or --profile, then:
org.gradle.caching=true), local and remote (shared by CI and developers).org.gradle.configuration-cache=true).org.gradle.parallel=true) and a healthy module graph.org.gradle.jvmargs=-Xmx4g).tasks.register, providers), and avoid work at configuration time.maxParallelForks, test filtering, and moving slow integration tests out of the default check.Short answer: Put the tests in src/test/java, add JUnit 5, and tell Gradle to use the JUnit Platform:
dependencies {
testImplementation(platform("org.junit:junit-bom:5.11.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.mockito:mockito-junit-jupiter:5.12.0")
testImplementation("org.assertj:assertj-core:3.26.0")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
testLogging { events("failed"); exceptionFormat = TestExceptionFormat.FULL }
}
Run them with ./gradlew test. The reports end up in build/reports/tests/test.
Common trap: JUnit 4 (junit:junit:4.12) examples. Without useJUnitPlatform(), JUnit 5 tests silently don't run. Use the JVM Test Suite plugin for separate integration-test suites.
Short answer: Enable it with org.gradle.caching=true. Cacheable tasks (compilation, tests, code generation) store their outputs keyed by a hash of their inputs. Any later build, on any branch or machine, with the same inputs reuses the outputs (FROM-CACHE) instead of running the task. A remote cache (Develocity, or a cache node) lets CI populate the cache and developers consume it.
// settings.gradle.kts
buildCache {
local { isEnabled = true }
remote<HttpBuildCache> {
url = uri("https://gradle-cache.acme.internal/cache/")
isPush = System.getenv("CI") != null // only CI writes; developers read
}
}
Key points to cover:
Short answer: Apply maven-publish, define a publication (the component, plus the sources and Javadoc JARs), and a repository, with credentials from the environment. Then run ./gradlew publish.
plugins { `java-library`; `maven-publish` }
java { withSourcesJar(); withJavadocJar() }
publishing {
publications { create<MavenPublication>("lib") { from(components["java"]) } }
repositories {
maven {
name = "company"
url = uri(if (version.toString().endsWith("SNAPSHOT"))
"https://nexus.acme.internal/repository/snapshots/"
else "https://nexus.acme.internal/repository/releases/")
credentials(PasswordCredentials::class) // reads companyUsername / companyPassword properties
}
}
}
Key points to cover:
signing for public releases (Maven Central requires it).Short answer: Apply the quality plugins (Spotless, Checkstyle, PMD, SpotBugs, JaCoCo with coverage verification, Error Prone, and OWASP Dependency-Check), preferably in a convention plugin, so every module gets them. Wire them into check, so ./gradlew build enforces them, and fail on violations. Report to SonarQube from CI.
plugins { checkstyle; jacoco; id("com.diffplug.spotless"); id("com.github.spotbugs") }
tasks.check { dependsOn(tasks.jacocoTestCoverageVerification) }
tasks.jacocoTestCoverageVerification { violationRules { rule { limit { minimum = "0.80".toBigDecimal() } } } }
spotless { java { googleJavaFormat() } }
Short answer: Centralise the version, not the dependency itself:
libs.versions.toml), so every module references libs.jackson.databind with one version.java-platform project with constraints, to align families of libraries.failOnVersionConflict(), to detect drift.Each module still declares only the dependencies it uses.
Common trap: declaring the dependency inside subprojects { dependencies { implementation(...) } }. That forces the library into every module, whether it needs it or not, and cross-project configuration is discouraged anyway.
Short answer: Implement Plugin<Project>, and in apply(), apply other plugins, register tasks and extensions (the configurable DSL). The simplest form is a precompiled script plugin: a acme.java-conventions.gradle.kts file in build-logic/src/main/kotlin. For distribution across repositories, build a standalone plugin with java-gradle-plugin, test it with TestKit, and publish it to the company repository (or the Gradle Plugin Portal).
class ServiceConventionsPlugin : Plugin<Project> {
override fun apply(project: Project) = with(project) {
pluginManager.apply("java")
extensions.configure<JavaPluginExtension> { toolchain.languageVersion.set(JavaLanguageVersion.of(21)) }
tasks.withType<Test>().configureEach { useJUnitPlatform() }
}
}
Use cases: company-wide conventions (the Java version, quality checks, publishing), code generation, deployment tasks, and integrating internal tools.
Short answer: The Wrapper (gradlew plus gradle/wrapper/gradle-wrapper.properties) pins the Gradle version per project, and downloads it automatically. Every developer and CI agent builds with exactly the same version, with no installation and no "works with my Gradle" problems. Upgrades are a reviewed change (./gradlew wrapper --gradle-version 8.10).
Key points to cover:
gradle-wrapper.jar, and verify the distribution checksum (distributionSha256Sum) for supply-chain safety.Q: What are Gradle's three build phases?
A: Initialisation (read settings.gradle, determine the projects), configuration (evaluate the build scripts, build the task graph) and execution (run the selected tasks). Keeping configuration cheap, and cacheable, is key to performance.
Q: What's the difference between tasks.register and tasks.create?
A: register is lazy: the task is configured only if it's actually needed in this build. create configures it eagerly, which slows every build.
Q: How do you run a single test class?
A: ./gradlew test --tests "com.acme.orders.CheckoutServiceTest", where wildcards and method names are supported.
Q: What is Develocity (formerly Gradle Enterprise)? A: A commercial platform for build scans, remote build caching, predictive test selection and build analytics, for both Gradle and Maven.