Overriding versions in multi-module builds, dependencyManagement vs dependencies, BOMs, parent/child POMs, profiling builds, compiler and Surefire plugins, shaded/fat JARs, install vs package vs deploy, Gradle api vs implementation, conditional plugins and dependencies, cross-module task ordering, running specific tasks and tests, project properties, activating profiles, multi-module best practices, sharing configuration (convention plugins, version catalogs), test artifacts, parallel builds, and Groovy vs Kotlin DSL.
Published September 25, 2026
Build questions at this level are about reproducibility, speed and governance across many modules and teams:
Short answer:
Maven:
<dependencyManagement>, which controls the version for every module, including transitive occurrences;<jackson-bom.version>2.18.2</jackson-bom.version>), which is the cleanest way to bump a Boot-managed library;scope import), declare your override before the Boot BOM in dependencyManagement (the first declaration wins).Verify with mvn dependency:tree -Dincludes=groupId:artifactId.
Gradle:
libs.versions.toml), or a platform;constraints { implementation("…:2.18.2") }, or strictly versions, for enforcement;resolutionStrategy.force only as a last resort.Inspect with ./gradlew dependencyInsight --dependency jackson-databind.
dependencyManagement and dependencies in Maven?Short answer:
<dependencies> actually adds the dependencies to the module's classpath (and they're inherited by child modules, if declared in a parent).<dependencyManagement> only declares versions, scopes and exclusions for dependencies if and when a module uses them. It doesn't add them. Child modules then declare the dependency without a version. That's how you get consistent versions across modules, without forcing every module to include every library. Importing a BOM (<type>pom</type><scope>import</scope>) inside dependencyManagement pulls in a whole curated version set.Short answer: A BOM is a POM that contains only dependencyManagement, for a coherent set of artifacts: spring-boot-dependencies, jackson-bom, testcontainers-bom, aws-sdk-bom, or your organisation's platform BOM. Importing it gives:
<parent>).Gradle's equivalent is implementation(platform("…:bom:x")) (or enforcedPlatform).
Short answer:
<parent>): a child inherits the parent's groupId/version, properties, dependencyManagement, pluginManagement, dependencies, plugin configuration, repositories and profiles. It can override them. It's used for shared build conventions (the compiler release, plugin versions, Surefire configuration, quality plugins). The Spring Boot starter parent is an example.<modules>): a POM with packaging pom lists modules, so one build runs them all, in reactor order (computed from their inter-dependencies).relativePath carefully when the parent isn't in the directory above.Short answer:
Measure:
-Dprofile isn't built in, so use the Maven Build Time Profiler extension or buildtime-maven-extension;-X/-B and -Dorg.slf4j.simpleLogger.showDateTime=true;Find the slow plugins and modules (tests, annotation processing, shading).
Speed it up:
-T 1C);-pl moduleA -am);~/.m2;mvnd) for local builds;dependency:analyze).Maven profiles (<profiles>) are a different concept: environment- or condition-specific build configuration (see Q13).
maven-compiler-plugin and maven-surefire-plugin for?Short answer:
maven-compiler-plugin: compiles src/main/java and src/test/java. Configure it with <release>21</release> (preferred over source and target, because it also checks the API usage against that JDK), with annotation processors (annotationProcessorPaths for Lombok, MapStruct, the Spring configuration processor, Hibernate JPA metamodel), -parameters (needed for Spring parameter-name binding), and compiler warnings (-Xlint).maven-surefire-plugin: runs unit tests in the test phase (JUnit 5 through the JUnit Platform), with includes and excludes, forking and parallelism, system properties, and reports. Failsafe runs integration tests (*IT) in the integration-test and verify phases, so post-integration cleanup (stopping containers) still happens before the build fails.Short answer:
spring-boot-maven-plugin (repackage). It creates an executable JAR with nested JARs (BOOT-INF/lib) and a custom launcher. It isn't shading: no class relocation is needed.maven-shade-plugin, which merges all dependency classes into one JAR. It can relocate packages (com.google.common → myapp.shaded.guava), to avoid conflicts with the host application's versions (important for libraries and Hadoop/Spark jobs). Transformers merge META-INF/services files and set the Main-Class. maven-assembly-plugin (jar-with-dependencies) is the older, simpler alternative, without relocation.package, install and deploy goals in Maven?Short answer: They're lifecycle phases (each runs all the earlier phases):
package: compile, test, and build the artifact (JAR or WAR) in target/.install: package, plus verify, then copy the artifact into the local repository (~/.m2/repository), so other local projects can depend on it.deploy: install, then upload the artifact to a remote repository (Nexus, Artifactory, GitHub Packages), as configured in distributionManagement. It's used in CI for releases and snapshots.mvn clean install in CI is often unnecessary. mvn verify is enough, unless downstream local modules need the artifacts.
implementation and api in Gradle?Short answer: Both come from the java-library plugin:
api: the dependency is part of this module's public API (its types appear in public method signatures). It leaks to consumers' compile classpaths.implementation: an internal dependency. It's on this module's compile classpath, and the consumers' runtime classpath, but not on their compile classpath.The benefits of implementation:
Use api only when necessary. Other configurations: compileOnly (like Maven's provided: Lombok, annotations), runtimeOnly (JDBC drivers), testImplementation, and annotationProcessor.
Short answer:
if (providers.gradleProperty("withNative").isPresent) {
apply(plugin = "org.graalvm.buildtools.native")
}
dependencies {
if (project.hasProperty("postgres")) runtimeOnly("org.postgresql:postgresql")
else runtimeOnly("com.h2database:h2")
}
pluginManager.withPlugin("java") { … }: react when another plugin is applied (common in convention plugins).providers.gradleProperty, providers.environmentVariable), so the configuration cache works. Avoid System.getenv reads at configuration time.Short answer:
dependsOn (task A needs B's output), or better, wire the task inputs to the outputs of the other tasks (from(project(":api").tasks.named("openApiGenerate"))), so Gradle infers the ordering and caching correctly.mustRunAfter/shouldRunAfter (for example, integration tests after unit tests if both run).finalizedBy, for cleanup tasks.dependsOn(":module:task"), or aggregation tasks in the root. Avoid cross-project configuration (allprojects {} mutating others); prefer convention plugins.Short answer:
./gradlew :orders:test --tests "com.shop.orders.CheckoutServiceTest" # one class
./gradlew test --tests "*CheckoutServiceTest.rejectsEmptyCart" # one method
./gradlew :orders:integrationTest -PincludeTags=slow # a custom property
./gradlew build -x test # exclude a task
./gradlew :orders:bootRun --args='--spring.profiles.active=local'
./gradlew tasks --all # list tasks
./gradlew test --rerun # force a rerun (ignore up-to-date)
./gradlew build --scan # a build scan for diagnostics
In Maven: mvn -pl orders -Dtest=CheckoutServiceTest#rejectsEmptyCart test, and -Dit.test=... for Failsafe.
Short answer:
gradle.properties (project, or user home ~/.gradle), -Pname=value on the command line, or ORG_GRADLE_PROJECT_name environment variables. Read them with providers.gradleProperty("name"). They're used for versions, feature switches, credentials (from the user-home properties, never committed), and environment selection.<properties> in the POM, -Dname=value, and settings.xml.<profiles>, activated with -Pprod, or automatically by JDK, OS, property or file presence;-Penv=prod) and conditional logic, or separate tasks.Short answer:
api vs implementation (Gradle) to limit leakage.dependencyManagement and BOMs;gradle/libs.versions.toml) and platforms.pluginManagement and shared plugin configuration;build-logic (an included build, id("shop.java-conventions")), not allprojects/subprojects blocks, which couple the projects and break configuration caching.mvnw/gradlew) committed to the repository, pinned plugin versions, and dependency locking or verification (gradle/verification-metadata.xml).Short answer:
maven-jar-plugin's test-jar goal produces *-tests.jar. Consumers depend on it with <type>test-jar</type> (or <classifier>tests</classifier>) and <scope>test</scope>. It's used to share test utilities or fixtures. The cleaner alternative is a dedicated *-test-support module.java-test-fixtures plugin: src/testFixtures/java, with consumers using testImplementation(testFixtures(project(":orders"))). Or publish an extra artifact with a classifier.Prefer test-support modules or test fixtures over exporting whole test source sets, which drags in the test dependencies and the tests themselves.
Short answer:
mvn -T 1C verify (one thread per core; or -T 4) builds independent modules in parallel (following the reactor graph);parallel/forkCount), and mvnd.org.gradle.parallel=true (in gradle.properties) runs tasks of decoupled projects in parallel;maxParallelForks for test JVMs;org.gradle.workers.max;org.gradle.configuration-cache=true) and build cache (org.gradle.caching=true), for bigger wins.build.gradle.kts and build.gradle?Short answer:
build.gradle uses the Groovy DSL: dynamic and concise, with weaker IDE support and fewer compile-time errors.build.gradle.kts uses the Kotlin DSL: statically typed, with excellent IDE auto-completion, navigation and refactoring, and compile-time errors. The first configuration can be slightly slower (script compilation, which is cached). It's the default for new Gradle projects (and Spring Initializr offers it).Both configure the same Gradle API. Kotlin is recommended for maintainability, especially with convention plugins in build-logic.
Q: What is the Gradle configuration cache? A: It caches the result of the configuration phase (the task graph), so later builds skip configuration entirely. That's a big speed-up for large builds. It requires build logic to avoid configuration-time side effects, and to use lazy providers.
Q: How do you prevent dependency-confusion attacks?
A: Resolve internal group IDs only from the internal repository (Gradle exclusiveContent/repository content filtering, and Maven mirror settings routing everything through Nexus or Artifactory), and use dependency verification (checksums or signatures).
Q: What does mvn dependency:analyze report?
A: "Used undeclared" dependencies (you rely on transitive ones, so declare them explicitly) and "unused declared" dependencies (candidates for removal). It keeps the dependency graph honest.
Q: Maven or Gradle for a new Spring Boot project? A: Both are fully supported. Maven means convention and simplicity, and it's ubiquitous. Gradle means faster incremental builds, caching and flexible logic, at the cost of more complexity. Choose based on the team's familiarity and the build's complexity.