Spring Boot Build Tools: Maven & Gradle

Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.

published: reading time: 28 min read author: GeekWorkBench
Quick Summary

Configure Maven and Gradle for Spring Boot projects,plugins, dependency management, packaging JARs and WARs, and build automation essentials. The guide uses practical examples to explain introduction to spring boot build tools, maven configuration and shows how to apply the ideas in a Spring Boot project. It closes with common pitfalls and production checks so you can apply the pattern with fewer surprises.

Spring Boot Build Tools: Maven & Gradle

Spring Boot projects live or die by their build configuration. The build tool sits at the center of everything—pulling in dependencies, running tests, packaging the application, and deploying it. Get the build wrong and nothing else matters. This guide covers Maven and Gradle from the ground up, with Spring Boot–specific plugins, dependency management patterns, and the packaging decisions that determine how your application runs in production.

Introduction to Spring Boot Build Tools

Every Spring Boot project needs a build tool. The two standard options are Maven, with its XML-based pom.xml, and Gradle, with its Groovy or Kotlin DSL build scripts. Both integrate with Spring Boot through dedicated plugins that handle dependency management, auto-configuration, packaging, and running the application.

Spring Boot’s build plugins do the heavy lifting. The spring-boot-maven-plugin and spring-boot-gradle-plugin produce an executable fat JAR that bundles your application code alongside all its dependencies. This is what makes java -jar deployment practical—no application server installation required on the target machine.

The choice between Maven and Gradle rarely comes down to capability. Both can build production-ready Spring Boot applications. The real difference is ergonomics: Maven uses XML and enforces a rigid project structure, while Gradle offers a DSL that scales from simple scripts to complex multi-project builds. Pick the tool your team knows best, then lean into its conventions.

Maven Configuration

2.1 The pom.xml Structure

A Spring Boot Maven project centers on the pom.xml file. The critical piece is the spring-boot-starter dependency hierarchy and the spring-boot-maven-plugin.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.1</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>my-spring-boot-app</artifactId>
    <version>1.0.0</version>
    <name>My Spring Boot Application</name>

    <properties>
        <java.version>21</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!-- Test dependencies -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

The spring-boot-starter-parent is the key to the whole dependency management system. It locks dependency versions, sets default plugin configurations, and inherits plugin management from Spring Boot’s BOM (Bill of Materials). You never specify version numbers for Spring Boot starters when using the parent—version management flows down from the parent POM.

2.2 Spring Boot Maven Plugin

The spring-boot-maven-plugin is what transforms a standard JAR into an executable fat JAR. Without it, your application would require all dependencies on the classpath at runtime. With it, the plugin repackages your artifact to include every dependency under BOOT-INF/lib/.

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <excludes>
            <exclude>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
            </exclude>
        </excludes>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
                <goal>build-info</goal>
            </goals>
        </execution>
    </executions>
</plugin>

The repackage goal runs automatically during the package phase. It takes the JAR produced by Maven and replaces it with the fat JAR. The build-info goal generates build metadata (application version, Java version, build timestamp) that Spring Boot Actuator exposes automatically at /actuator/info.

2.3 Multi-Module Maven Projects

For larger applications, split the build into multiple modules. A common structure separates domain code from the web layer.

my-project/
├── pom.xml                    ← Parent POM
├── module-domain/
│   └── pom.xml
├── module-web/
│   └── pom.xml
└── module-archive/
    └── pom.xml
<!-- Parent pom.xml -->
<project>
    <groupId>com.example</groupId>
    <artifactId>my-project</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <modules>
        <module>module-domain</module>
        <module>module-web</module>
        <module>module-archive</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.4.1</version>
    </parent>
</project>

Gradle Configuration

3.1 The build.gradle Structure

Gradle uses a DSL that is more expressive than Maven’s XML. The equivalent Spring Boot Gradle build looks like this:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.1'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '1.0.0'

java {
    sourceCompatibility = JavaVersion.VERSION_21
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'

    runtimeOnly 'org.postgresql:postgresql'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

The io.spring.dependency-management plugin is what activates Spring Boot’s dependency management in Gradle. Without it, you would need to specify version numbers for every dependency. With it, the Spring Boot plugin manages versions the same way Maven’s starter-parent does.

3.2 Spring Boot Gradle Plugin

The Spring Boot Gradle plugin handles repackaging, but its configuration looks different from Maven’s XML approach.

bootJar {
    archiveFileName = "${project.name}-${project.version}.jar"
}

bootRun {
    sourceResources sourceSets.main
}

tasks.register('bootJarInfo', BootInfoTask) {
    enabled = true
}

The plugin automatically applies repackage during the build lifecycle. Run ./gradlew bootJar to produce the executable fat JAR, or ./gradlew bootBuildImage to build a container image using Cloud Native Buildpacks.

3.3 Kotlin DSL

If your team prefers Kotlin syntax, use build.gradle.kts:

import org.springframework.boot.gradle.tasks.bundling.BootJar

plugins {
    java
    id("org.springframework.boot") version "3.4.1"
    id("io.spring.dependency-management") version "1.1.7"
}

group = "com.example"
version = "1.0.0"

java {
    sourceCompatibility = JavaVersion.VERSION_21
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.named<BootJar>("bootJar") {
    archiveFileName.set("${project.name}-${project.version}.jar")
}

Dependency Management Best Practices

4.1 Starters are Your Friend

Spring Boot starters are curated dependency sets that follow the convention spring-boot-starter-*. The spring-boot-starter alone pulls in auto-configuration, the Spring context, and logging. Add spring-boot-starter-web and you get MVC, REST, and embedded Tomcat. Add spring-boot-starter-data-jpa and you get Hibernate, transaction management, and Spring Data abstractions.

Never manually assemble the dependencies that a starter provides. Using spring-boot-starter-web rather than listing spring-web, spring-webmvc, tomcat-embed-core, and jackson-databind separately means Spring Boot manages the versions—and more importantly, those versions are tested together as a coherent set.

4.2 Avoiding the Dependency Hell

When multiple libraries pull in conflicting transitive dependencies, use Maven’s dependency tree or Gradle’s dependency insight to trace the conflict.

# Maven: see where a transitive dependency comes from
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core

# Gradle: see why a dependency is included
./gradlew dependencies --configuration runtimeClasspath
./gradlew dependencyInsight --dependency jackson-databind

In Gradle, exclude transitive dependencies explicitly:

implementation('some:library') {
    exclude group: 'org.springframework', module: 'spring-core'
}

In Maven, use an exclusion:

<dependency>
    <groupId>some</groupId>
    <artifactId>library</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

4.3 Managing Dependency and Plugin Versions

One version mismatch rule: never hardcode a version that Spring Boot’s parent already manages. If you want to override a managed version, do it explicitly:

<!-- In Maven, property precedence lets you override -->
<properties>
    <spring-core.version>6.1.2</spring-core.version>
</properties>
// In Gradle, use the dependency management plugin
dependencyManagement {
    resolutionStrategy {
        force 'org.springframework:spring-core:6.1.2'
    }
}

Packaging: JAR vs WAR

5.1 Executable JAR (Default)

Spring Boot’s default packaging is an executable JAR. The fat JAR contains everything needed to run the application with java -jar. No external application server required.

mvn clean package
java -jar target/my-app-1.0.0.jar
./gradlew build
java -jar build/libs/my-app-1.0.0.jar

This approach works for microservices, containerized applications, and any scenario where you control the runtime environment. The container community has converged on this model—Docker images based on eclipse-temurin or amazoncorretto images run Spring Boot fat JARs directly.

5.2 Traditional WAR Deployment

Some environments still require WAR deployment to an external servlet container. Switch packaging to war and mark the embedded server as provided:

<packaging>war</packaging>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>

In Gradle:

apply plugin: 'war'

dependencies {
    providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
}

Spring Boot’s main class must then extend SpringBootServletInitializer:

public class Application extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(Application.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Common Build Tasks

6.1 Running Tests

# Maven
mvn test
mvn verify                    # Runs tests + integration tests

# Gradle
./gradlew test
./gradlew verify

6.2 Building Without Tests

mvn package -DskipTests
./gradlew build -x test

6.3 Running the Application

# Maven
mvn spring-boot:run
mvn spring-boot:run -Dspring-boot.run.arguments='--server.port=9000'

# Gradle
./gradlew bootRun
./gradlew bootRun --args='--server.port=9000'

6.4 Building Docker Images

# Maven with Jib
mvn com.google.cloud.tools:jib-maven-plugin:build

# Gradle with Jib
./gradlew jibDockerBuild

Spring Boot’s built-in container building:

# Maven
mvn spring-boot:build-image

# Gradle
./gradlew bootBuildImage

Common Pitfalls

Mixing dependency scopes incorrectly. Runtime dependencies should use runtime (Maven) or runtimeOnly (Gradle) — they are not on the compilation classpath. A dependency in the wrong scope produces a NoClassDefFoundError at runtime even though everything compiled without issue.

Overriding managed versions without understanding compatibility. Spring Boot’s dependency management is a tested set. Bumping spring-core to a newer minor version than the parent specifies can break things that depend on internal APIs. Only override versions when you have a specific reason and have tested the combination.

Forgetting the Spring Boot Maven plugin configuration. Without the plugin, mvn package produces a thin JAR that cannot be run with java -jar. If you need traditional WAR deployment, switch the packaging type or explicitly skip the repackage goal.

Skipping the dependency management plugin in Gradle. Without io.spring.dependency-management, you lose Spring Boot’s version management entirely. Every transitive dependency needs an explicit version, and you inherit full responsibility for version conflicts across the entire dependency tree.

When to Use / When NOT to Use

When to Use Maven

Maven is the right choice when your team has prior experience with it and your project follows conventional conventions. It shines in environments where dependency resolution through Maven Central is the primary concern, because Maven’s dependency metadata model and repository infrastructure are battle-tested and widely understood. Multi-module enterprise builds also favor Maven’s strict project structure and lifecycle enforcement—modules declare their relationships explicitly, and the reactor can order builds correctly without additional configuration.

Use Maven when your organization already has established Maven infrastructure: internal Maven repositories, company-wide parent POMs, or CI pipelines built around Maven goals. Most build problems you run into will have documented solutions because the Maven plugin ecosystem is well-established.

When to Use Gradle

Gradle makes sense on complex builds where Maven’s XML becomes a liability. If you need fine-grained control over task execution order, incremental compilation, or custom build logic that runs across many modules, Gradle’s programming model handles that naturally. The Kotlin DSL gives you type-safe build scripts with IDE support that catches errors before you run the build.

Gradle’s incremental build model cuts build times significantly on large projects because it tracks inputs and outputs at the task level and skips work already done. Build caching and parallel execution are easier to configure in Gradle than in Maven’s lifecycle model.

When NOT to Use Maven

Do not reach for Maven when you need to express build logic that does not map cleanly onto the standard lifecycle phases. If your build requires significant scripting around conditional logic, dynamic artifact generation, or complex orchestration that fights Maven’s conventions, you will spend more time working around the tool than using it.

When NOT to Use Gradle

Avoid Gradle when your team has no experience with it and the project is simple—a straightforward JAR or WAR with a handful of dependencies. The flexibility that makes Gradle powerful also makes it easier to create unmaintainable build scripts. Kotlin DSL has a learning curve, and Gradle’s behavior can differ between versions in ways that are harder to debug than Maven’s more predictable lifecycle.

Trade-Off Table

Dimension Maven Gradle
Build Speed Standard execution; incremental builds limited Incremental by default; build cache reduces rebuilds significantly
DSL Flexibility XML-based; strictly structured Groovy or Kotlin DSL; full programming language expressiveness
Plugin Ecosystem Large and mature; most tools have Maven plugins Growing rapidly; some tools lag Maven equivalents
Learning Curve Shallow for basics; XML conventions are predictable steeper—DSL syntax, task model, and convention-over-configuration interactions
CI/CD Integration Excellent; wide tool support and documented patterns Excellent; native support in most modern CI platforms
Multi-Module Builds Strong reactor with explicit module ordering Strong; fine-grained task dependency control
Configuration Complexity Low for standard projects; grows with custom needs Lower for custom needs; higher for standard projects due to flexibility
IDE Support Universal; excellent support across all major IDEs Very good; Kotlin DSL IDE support improving rapidly
Dependency Management BOM and dependency plugin; transitive management by exclusion BOM via plugin; better resolution strategy control
Build Reproducibility Excellent when lifecycle is followed Excellent with proper task configuration

Observability Checklist

Build pipeline observability catches problems before they reach production. If you do not monitor your CI/CD pipeline the same way you monitor your running services, you will miss slow builds, flaky tests, and dependency regressions until they hit users.

Build Duration Metrics

  • Track build duration per module and alert on regression. A sudden spike in module build time often indicates a dependency being resolved from scratch or a plugin misbehaving.
  • Record mvn test and ./gradlew test times over time. Set thresholds that trigger investigation when test suite duration increases beyond 20% from baseline.

Dependency Version Drift Detection

  • Run mvn dependency:tree or ./gradlew dependencies in CI and diff outputs against a baseline. Unexpected transitive dependency additions or version changes should block merges.
  • Use tools like Dependabot or Renovate to automate dependency update pull requests and track update history.

Plugin Failure Alerting

  • Every CI run should produce a structured log that marks pass/fail at the plugin level, not just the goal or task level. Maven’s –fail-at-end can mask partial failures in multi-module builds.
  • Alert on repeated plugin download failures—a corrupted or unavailable artifact cache causes intermittent build failures that are hard to diagnose.

Artifact Integrity Checksums

  • Generate and verify SHA-256 checksums for every published artifact. Store checksums alongside artifacts in your repository or artifact registry.
  • For internal artifacts, verify checksums before promoting to a staging environment.

Maven/Gradle Daemon Memory Usage

  • Gradle daemon memory usage should be monitored in CI. Configure org.gradle.jvmargs to set heap limits and prevent daemon OOM kills that produce cryptic build failures.
  • Maven daemon (if using Maven daemonizers) similarly needs memory limits.

Security and Compliance Notes

Build security does not get attention until something breaks. The time to integrate security scanning, artifact signing, and license checks into your build pipeline is before you have a problem.

Dependency Vulnerability Scanning

Run OWASP Dependency-Check as part of your build. It cross-references your dependency tree against the National Vulnerability Database (NVD):

<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <configuration>
        <failBuildOnCVSS>7</failBuildOnCVSS>
    </configuration>
</plugin>
tasks.register('dependencyCheck', DependencyCheckTask) {
    failBuildOnCVSS = 7
    suppressionFile = file('dependency-check-suppressions.xml')
}

Signing Build Artifacts

Sign artifacts destined for release repositories using GPG for Maven or the Gradle signing plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-gpg-plugin</artifactId>
    <configuration>
        <passphrase>${env.GPG_PASSPHRASE}</passphrase>
    </configuration>
</plugin>
signing {
    useGpgCmd()
    sign configurations.archives
}

Managing Credentials

Never hardcode credentials in pom.xml, build.gradle, or any build script. Use environment variables or CI secrets:

<plugin>
    <configuration>
        <password>${env.MAVEN_REPO_PASSWORD}</password>
    </configuration>
</plugin>
publishing {
    repositories {
        maven {
            url = uri("https://repo.example.com/releases")
            credentials {
                username = System.getenv("MAVEN_REPO_USER")
                password = System.getenv("MAVEN_REPO_PASSWORD")
            }
        }
    }
}

License Compliance Checking

Add the license-maven-plugin or the Gradle equivalent to verify that all dependencies have compatible licenses before release:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>license-maven-plugin</artifactId>
    <configuration>
        <failOnUnknownLicenses>true</failOnUnknownLicenses>
    </configuration>
</plugin>

Private Maven Repository Access Control

If using private repositories (GitHub Packages, Artifactory, Nexus), enforce access control through CI environment variables rather than sharing credentials in configuration files. Rotate credentials regularly and revoke immediately after team member departure.

Real-world Failure Scenarios

Build failures in Spring Boot projects rarely come from application code—they come from the build. These are the scenarios that surface most often in production CI/CD pipelines and how to recover from them.

Build Succeeds, Application Crashes on Startup with NoClassDefFoundError

This is the most common build-classpath mismatch. The application compiled cleanly, all tests passed, but the running application cannot find a class. The culprit is usually a runtime-scoped dependency that was accidentally moved to compile scope. In Maven, check for a missing <scope>runtime</scope> on database drivers or connection pool libraries. In Gradle, verify that runtime-only dependencies use runtimeOnly and not implementation. The fix is to move the dependency to the correct scope—compile-scoped runtime dependencies vanish from the classpath when the fat JAR repackages.

Fat JAR Runs Locally but Fails in Kubernetes with ClassNotFoundException or NoSuchMethodError

This typically means your local environment and the container image are resolving different versions of a transitive dependency. A library on your local Maven/Gradle cache may not match what gets bundled from the build. Run mvn dependency:tree or ./gradlew dependencies inside the container build context and diff against your local output. Look for version conflicts where two libraries pull in different major versions of the same transitive dependency. The solution is to explicitly exclude one version and force a consistent version up the chain.

Gradle Build Cache Returns Stale Classes After Dependency Update

Incremental compilation can cache stale bytecode when a dependency API changes but not the source signatures your code depends on. The build reports success but the running application throws NoSuchMethodError because it is running old compiled classes that do not match the new dependency. Run ./gradlew clean build to invalidate the incremental cache. If it recurs, disable the build cache for that specific task with outputs.cacheIf { false } while you investigate which task is producing incorrect cache keys.

Maven Multi-Module Build Orders Modules Incorrectly

Maven’s reactor determines build order from the dependency graph, but if module A depends on module B’s compiled artifact rather than its source, the reactor may schedule them in the wrong order when you run targets in parallel. The symptom is intermittent ClassNotFoundException in early modules. Fix this by using <skip>true</skip> on the plugin that produces the cross-module artifact or by restructuring the dependency to be source-based rather than artifact-based.

Spring Boot Plugin Skips Repackage and Produces Thin JAR in CI

This happens when the build runs as a different user than the one who originally ran mvn package, or when the CI environment uses a shared cache that partially corrupted the plugin state. The plugin detects that the JAR was already repackaged and skips the step on subsequent runs. Run mvn clean package to force a full rebuild. If it persists, delete the CI cache entirely and rebuild from scratch.

Build Fails with Could Not Resolve Dependencies After Network Partition to Maven Central

Cached resolution failures persist in the local repository metadata. Once Maven marks a dependency as unreachable, it does not retry until the cache expires. Delete the bad resolution from .m2/repository or run mvn -U to force updates. In Gradle, delete the corrupted entry from .gradle/caches/modules-2/files-2.1/ and rebuild.

Quick Recap Checklist

  • Using spring-boot-starter-parent for dependency management in Maven
  • spring-boot-maven-plugin or Spring Boot Gradle plugin configured for fat JAR
  • All Spring Boot dependencies use managed versions (no hardcoded versions for starters)
  • Test dependencies scoped correctly (test scope in Maven, testImplementation in Gradle)
  • Production database driver on runtime scope (not compile)
  • Application starts and runs healthily with java -jar target/*.jar
  • Multi-module project uses proper <relativePath/> for parent POM
  • Docker image builds successfully from the packaged JAR

Interview Questions

1. Why does Spring Boot produce a fat JAR, and what problem does it solve?

Spring Boot's executable fat JAR bundles the application's bytecode and all its transitive dependencies into a single artifact. The key problem it solves is dependency management at runtime—no need to install a separate application server or manage a classpath with dozens of JAR files. You deploy one file and run it with java -jar. The fat JAR also enables reproducible builds since the exact dependency versions are locked inside the archive.

2. What is the difference between Maven's spring-boot-starter-parent and the Spring Boot BOM?

The spring-boot-starter-parent is a special Maven POM that extends the Spring Boot BOM while also configuring plugin defaults, Java version, resource encoding, and other project-wide settings. Using the parent gives you both dependency version management and sensible build defaults in one step. If you cannot extend the parent (for example, in a multi-module project where your parent is a company-wide POM), you can import the Spring Boot BOM directly with dependencyManagement.import scope to get version management alone.

3. When should you package a Spring Boot application as a WAR instead of a JAR?

Package as a WAR when your deployment target is a traditional application server (Tomcat, WildFly, WebSphere) that you do not control and cannot run embedded containers on. This is common in legacy enterprise environments with centralized app server management. In all other cases—especially microservices, containers, and PaaS deployments—the executable JAR is simpler and preferred because it eliminates the application server as a separate deployment concern.

4. How do you debug dependency conflicts in a Spring Boot project?

In Maven, run mvn dependency:tree to see the full dependency graph and identify where an unwanted transitive dependency comes from. Then add an explicit exclusion to the offending dependency. In Gradle, use ./gradlew dependencies for the full tree and ./gradlew dependencyInsight --dependency <name> to focus on a specific library. Once identified, exclude the transitive dependency with an explicit exclusion block. Always verify the exclusion does not break any functionality by running tests after adding it.

5. What does the spring-boot-maven-plugin repackage goal do?

The repackage goal takes the ordinary JAR that Maven builds (containing only your application classes) and replaces it with an expanded fat JAR that embeds all dependencies under BOOT-INF/lib/. It also adds Spring Boot's layer tool metadata and a launch script that makes java -jar work. If you disable repackage, you get a thin JAR back. The plugin also supports a build-info goal that writes build metadata to META-INF/build-info.properties, which Actuator exposes at /actuator/info.

6. How does the io.spring.dependency-management plugin work in Gradle, and why is it necessary?

The plugin activates Spring Boot's Bill of Materials (BOM) in Gradle, mirroring what spring-boot-starter-parent provides in Maven. Without it, Gradle resolves dependency versions from remote repositories independently, and you must specify versions manually for every transitive dependency. With the plugin applied, versions declared in the Spring Boot BOM are automatically applied to dependencies that match—the same behavior you get from Maven's dependency management. It also allows you to override specific versions using resolutionStrategy when needed.

7. What is the difference between implementation, compileOnly, runtimeOnly, and testImplementation dependency configurations in Gradle?

implementation is the default scope—dependencies are compiled against and included in the runtime classpath, but are not exposed to consumers of the library. compileOnly dependencies are available at compile time but not at runtime (useful for annotation processors and compile-time helpers). runtimeOnly dependencies are not on the compile classpath but are present at runtime (useful for database drivers that are loaded via reflection). testImplementation dependencies are only available during test compilation and execution. Using the wrong scope—particularly using implementation where runtimeOnly is correct—causes classpath pollution and can lead to NoClassDefFoundError in production.

8. What is the purpose of the Spring Boot layer tool in the fat JAR, and how do you use it?

Spring Boot's layer tool splits the fat JAR contents into logically separated layers: dependencies, application classes, and Spring Boot loader classes. This layering enables faster container builds in Docker layers—dependency layers change infrequently and can be cached while application layers rebuild on every code change. Run java -Djarmode=layertools jar extract to explode the fat JAR into layers, then use a multi-stage Dockerfile to copy only the layers you need. This reduces image rebuild time significantly in CI/CD pipelines where dependencies change less often than application code.

9. How do you handle different environments (dev, staging, prod) with Maven or Gradle build profiles?

In Maven, use profiles activated by environment or explicit flag: <profile><id>dev</id><properties>...</properties></profile> and activate with mvn package -Pdev. Spring Boot's @ActiveProfiles annotation picks up Spring-managed profiles from the build. In Gradle, use source sets or task conventions per environment: define separate application main classes or configuration files per environment and switch them with bootRun arguments or environment variables. Both approaches should keep environment-specific configuration out of the build artifact—use environment variables or external configuration servers rather than hardcoding URLs or credentials in the build.

10. What is the difference between mvn test and mvn verify in Maven?

mvn test runs the unit test phase only—the test goal on each project with a test task. mvn verify runs the full build lifecycle through integration test phase, which includes test plus any integration tests bound to the verify phase. In a Spring Boot project, verify also runs the spring-boot:run hooked into integration tests if configured, and any plugin goals bound to phases after test but before install. Use verify when you want to test the packaged artifact end-to-end, not just the compiled classes.

11. How does Gradle's incremental build model work, and what are its implications for build correctness?

Gradle tracks task inputs (files, properties) and outputs (compiled classes, generated resources) and skips a task if its inputs and outputs have not changed since the last run. This makes builds dramatically faster on unchanged code. The risk is stale caches: if a task's inputs are not fully declared (for example, a task reads a file but does not declare it as an input), Gradle may skip it when it should run. Always verify that custom tasks declare all inputs and outputs correctly. Use ./gradlew --rerun-tasks to force full rebuild when in doubt.

12. What is a Gradle build cache, and how does it differ from incremental builds?

The build cache stores task outputs across builds and machines. When enabled with org.gradle.caching=true, Gradle stores the results of cacheable tasks (identified by declared inputs and outputs) in a shared cache directory or remote cache server. On the next build—even a clean build—Gradle can download cached outputs instead of executing the task. This differs from incremental builds, which only skip work within the same build session when inputs have not changed. Build caching reduces CI/CD times significantly for projects with many modules, but requires that task inputs be fully and correctly declared.

13. What happens when you run mvn clean package versus mvn package in terms of artifact quality?

mvn package builds on top of any previously compiled classes, using whatever is already in target/. If source files were deleted or renamed since the last build, stale bytecode can remain and get packaged. mvn clean package deletes the entire target/ directory before building, ensuring a complete recompilation from a clean state. Always use clean when the dependency graph has changed, when switching branches, or when debugging mysterious runtime behavior that compilation should have caught. The small time cost of clean build is much less than the cost of debugging stale class artifacts.

14. How do you configure a Spring Boot multi-module Gradle project to share dependency versions?

Create a platform (or BOM) module that applies java-platform plugin and publishes a BOM that other modules consume. Alternatively, use the io.spring.dependency-management plugin in a convention plugin that all modules apply. The convention plugin approach is common in large projects: define a buildSrc/convention.gradle that applies the Spring Boot dependency management plugin and java conventions, then all application modules apply that plugin and automatically inherit version management without duplicating plugin declarations or version numbers.

15. What is the difference between Spring Boot's bootJar and bootWar tasks in Gradle?

bootJar is the default task that produces an executable fat JAR. bootWar is an alternative task activated when you apply the war plugin, and it produces a deployable WAR file instead. Both tasks replace the standard JAR/WAR output with Spring Boot's repackaged version. You cannot run both in the same build without additional configuration since they conflict. Choose bootJar for microservices and container deployments; choose bootWar when deploying to a managed application server.

16. Why should you avoid hardcoding Spring Boot dependency versions when using the starter parent?

The spring-boot-starter-parent locks all transitive dependency versions through Spring Boot's BOM. Hardcoding a version bypasses this tested configuration and can introduce version conflicts—the hardcoded version may be incompatible with other dependencies that expect a different version of the same library. For example, overriding spring-core to a newer minor version can break Hibernate or Jackson integrations that depend on specific internal APIs. Only override versions when you have a specific compatibility reason and have tested the combination end-to-end.

17. What is the difference between mvn dependency:tree and mvn dependency:list?

dependency:tree shows the full hierarchical dependency graph including transitive dependencies, making it suitable for understanding where a specific JAR comes from and identifying conflicts. dependency:list produces a flat, deduplicated list of direct and transitive dependencies by artifact coordinate. Use tree when you need to trace the origin of an unwanted transitive dependency; use list when you need a simple inventory of what ends up on the classpath. Both commands support -Dincludes filtering to narrow output to a specific group or artifact.

18. How do you configure Gradle to use a private Maven repository for dependencies?

Add the repository URL to the repositories block in build.gradle: maven { url 'https://repo.example.com/releases' credentials { username = System.getenv('MAVEN_REPO_USER'); password = System.getenv('MAVEN_REPO_PASSWORD') } }. For settings that should apply across all projects, configure it in settings.gradle using pluginManagement for plugin resolution and dependencyResolutionManagement for repositories. Never hardcode credentials in the build script—always use environment variables or gradle properties configured outside the repository.

19. What are the trade-offs between building a Docker image with Spring Boot's built-in bootBuildImage versus using Jib?

bootBuildImage uses Cloud Native Buildpacks to produce a distroless image with no shell, making it very secure and lean. Jib produces images using a layered approach without a Docker daemon, offering faster incremental builds and easier layer caching control. Jib allows you to customize the Dockerfile more freely, while bootBuildImage abstracts the container build entirely. Choose bootBuildImage for standard Spring Boot apps where you want zero Docker knowledge required; choose Jib when you need more control over the container build process or are integrating into an existing Docker-based CI/CD pipeline.

20. What is the Gradle daemon, and why does it get recommended to disable it in CI environments?

The Gradle daemon is a long-lived background process that persists JVM startup time across builds, making subsequent builds significantly faster. In local development, this is desirable. In CI environments, the daemon can cause issues: it retains memory across builds (requiring explicit memory configuration), can hold file locks that interfere with parallel job execution on the same agent, and may consume resources that CI infrastructure billing charges by usage. CI builds typically set org.gradle.daemon=false in gradle.properties or pass --no-daemon flag to ensure each build starts fresh without residual state.

Further Reading

Conclusion

Maven and Gradle are both mature, production-ready build tools for Spring Boot projects. Maven’s XML-based approach and strict conventions make it predictable and widely understood, while Gradle’s DSL provides greater flexibility for complex builds. The spring-boot-starter-parent in Maven and the io.spring.dependency-management plugin in Gradle both provide version management that eliminates the need to specify versions for Spring Boot starters.

Choose Maven for teams with existing Maven expertise and enterprise infrastructure. Choose Gradle for complex builds where incremental compilation, build caching, and fine-grained task control provide meaningful time savings. In both cases, use the Spring Boot plugins to produce executable fat JARs, and never hardcode dependency versions that Spring Boot already manages.

Category

Related Posts

Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow

Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.

#spring-boot #spring-boot-roadmap #learning-path

JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests

Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.

#spring-boot #spring-boot-roadmap #learning-path

Mockito: Mocking, Stubbing, Verifications, Spy vs Mock

Learn Mockito fundamentals: create mocks and spies, stub behavior with when().thenReturn(), verify interactions, and choose between Spy vs Mock wisely.

#spring-boot #spring-boot-roadmap #learning-path