Deep Dive: Java Modules and Build Tools

Building professional Java applications requires more than just writing code—you need robust build tools to manage dependencies, compile projects, run tests, and package deployables. In this deep dive, we'll explore Maven and Gradle, the two dominant build tools in the Java ecosystem, and cover the Java 9+ module system.
This is Phase 3 of our Java roadmap: mastering the build ecosystem that powers enterprise Java development.
Why Build Tools Matter
Modern Java projects have complex requirements:
- Dependency Management: Managing dozens or hundreds of libraries
- Build Automation: Consistent compilation, testing, and packaging
- Multi-Module Projects: Organizing large codebases
- IDE Integration: Seamless development experience
- CI/CD Integration: Automated builds and deployments
Without build tools, you'd manually download JARs, manage classpaths, and write complex shell scripts. Maven and Gradle solve these problems elegantly.
Maven: Convention Over Configuration
Maven has been the de facto standard for Java builds since 2004. It follows a strict "convention over configuration" philosophy with standardized project structures.
Project Structure
Maven enforces a standard directory layout:
my-app/
├── pom.xml # Project Object Model (build config)
├── src/
│ ├── main/
│ │ ├── java/ # Application source code
│ │ └── resources/ # Configuration files, properties
│ └── test/
│ ├── java/ # Test source code
│ └── resources/ # Test resources
└── target/ # Build output (generated)This structure is universal—every Maven project looks the same, making it easy to navigate unfamiliar codebases.
Understanding pom.xml
The pom.xml file is the heart of Maven projects. Here's a comprehensive example:
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- Project Coordinates (Maven GAV) -->
<groupId>com.chanhle</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- Project Metadata -->
<name>My Application</name>
<description>A sample Java application</description>
<!-- Properties (for version management) -->
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Dependency Versions -->
<spring.boot.version>3.2.1</spring.boot.version>
<junit.version>5.10.1</junit.version>
</properties>
<!-- Dependencies -->
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Lombok (compile-time only) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- JUnit 5 (test scope) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<!-- Build Configuration -->
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
<!-- Maven Surefire Plugin (for tests) -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.3</version>
</plugin>
<!-- Spring Boot Maven Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>Maven Coordinates: GAV
Every Maven artifact is identified by three coordinates (GAV):
- GroupId: Organization/namespace (e.g.,
com.chanhle) - ArtifactId: Project name (e.g.,
my-app) - Version: Version number (e.g.,
1.0.0-SNAPSHOT)
Dependency Scopes
Maven provides several dependency scopes:
| Scope | Build | Test | Runtime | Description |
|---|---|---|---|---|
compile | ✅ | ✅ | ✅ | Default scope, available everywhere |
provided | ✅ | ✅ | ❌ | Provided by runtime (e.g., servlet API) |
runtime | ❌ | ✅ | ✅ | Not needed for compilation (e.g., JDBC drivers) |
test | ❌ | ✅ | ❌ | Only for testing (e.g., JUnit) |
system | ✅ | ✅ | ✅ | Local JAR files (avoid if possible) |
import | N/A | N/A | N/A | Import dependency management from BOM |
Example:
<!-- Compile scope (default) -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
<!-- Provided scope (e.g., Lombok) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- Test scope (e.g., testing libraries) -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.8.0</version>
<scope>test</scope>
</dependency>Maven Build Lifecycle
Maven has three built-in lifecycles, each with phases:
1. Default Lifecycle (Build & Deploy)
# Key phases (in order):
mvn validate # Validate project structure
mvn compile # Compile source code
mvn test # Run unit tests
mvn package # Create JAR/WAR
mvn verify # Run integration tests
mvn install # Install to local repository (~/.m2/repository)
mvn deploy # Deploy to remote repository2. Clean Lifecycle
mvn clean # Delete target/ directory3. Site Lifecycle
mvn site # Generate project documentationPhase Chaining:
Each phase executes all previous phases:
mvn package # Runs: validate → compile → test → package
mvn clean install # Runs: clean → validate → compile → test → package → installMaven Plugins
Plugins extend Maven's functionality. Here are essential plugins:
Compiler Plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>21</source>
<target>21</target>
<compilerArgs>
<arg>--enable-preview</arg> <!-- Enable preview features -->
</compilerArgs>
</configuration>
</plugin>JAR Plugin (with Manifest):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>com.chanhle.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>Shade Plugin (Fat JAR):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.chanhle.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>Maven Profiles
Profiles allow environment-specific configurations:
<profiles>
<!-- Development Profile -->
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<env>development</env>
<database.url>jdbc:postgresql://localhost:5432/dev_db</database.url>
</properties>
</profile>
<!-- Production Profile -->
<profile>
<id>prod</id>
<properties>
<env>production</env>
<database.url>jdbc:postgresql://prod-server:5432/prod_db</database.url>
</properties>
<build>
<plugins>
<!-- Enable code obfuscation in production -->
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>2.6.0</version>
</plugin>
</plugins>
</build>
</profile>
</profiles>Usage:
mvn clean install # Uses dev profile (default)
mvn clean install -Pprod # Activates prod profileGradle: Flexible and Powerful
Gradle is a modern build tool that emphasizes flexibility and performance. It's the default build tool for Android and increasingly popular for server-side Java.
Key Advantages
- Performance: Incremental builds, build cache, parallel execution
- Flexibility: Programmatic build scripts (Groovy or Kotlin DSL)
- Conciseness: Less verbose than Maven XML
- Powerful: Can model complex build logic
Project Structure
Gradle uses the same directory structure as Maven but adds:
my-app/
├── build.gradle # Build configuration (Groovy)
├── settings.gradle # Multi-project settings
├── gradlew # Gradle Wrapper (Unix)
├── gradlew.bat # Gradle Wrapper (Windows)
├── gradle/
│ └── wrapper/
│ └── gradle-wrapper.properties
├── src/
│ ├── main/java/
│ └── test/java/
└── build/ # Build output (like target/)Groovy DSL vs Kotlin DSL
Gradle supports two DSLs:
Groovy DSL (build.gradle):
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.1'
}
group = 'com.chanhle'
version = '1.0.0-SNAPSHOT'
java {
sourceCompatibility = JavaVersion.VERSION_21
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}Kotlin DSL (build.gradle.kts):
plugins {
java
id("org.springframework.boot") version "3.2.1"
}
group = "com.chanhle"
version = "1.0.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_21
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.test {
useJUnitPlatform()
}Recommendation: Use Kotlin DSL for type safety and IDE autocomplete.
Gradle Configuration Scopes
Gradle uses different names for dependency scopes:
| Gradle | Maven Equivalent | Description |
|---|---|---|
implementation | compile | Available at compile and runtime |
api | compile | Like implementation but transitive |
compileOnly | provided | Compile-time only |
runtimeOnly | runtime | Runtime only |
testImplementation | test | Test compile and runtime |
testCompileOnly | test (provided) | Test compile only |
testRuntimeOnly | test (runtime) | Test runtime only |
Important: Use implementation instead of api unless you need transitive exposure.
Gradle Tasks
Tasks are the unit of work in Gradle:
# List all tasks
./gradlew tasks
# Common tasks
./gradlew clean # Delete build/
./gradlew compileJava # Compile source
./gradlew test # Run tests
./gradlew build # Full build (compile + test + package)
./gradlew bootRun # Run Spring Boot app
./gradlew bootJar # Create executable JARCustom Gradle Tasks
You can define custom tasks in build.gradle:
task hello {
doLast {
println 'Hello, Gradle!'
}
}
task copyResources(type: Copy) {
from 'src/main/resources'
into 'build/custom-resources'
}
task printClasspath {
doLast {
configurations.runtimeClasspath.each { println it }
}
}Run custom tasks:
./gradlew hello
./gradlew copyResourcesGradle Wrapper
The Gradle Wrapper (gradlew) is a game-changer:
- Version Consistency: Every developer uses the same Gradle version
- No Installation: Downloads Gradle automatically
- CI/CD Ready: No need to install Gradle in CI environments
Always use the wrapper:
./gradlew build # Unix/Linux/macOS
gradlew.bat build # WindowsInitialize wrapper:
gradle wrapper --gradle-version=8.5Dependency Management Deep Dive
Transitive Dependencies
When you declare a dependency, you get its dependencies too (transitive dependencies).
Example:
<!-- You declare Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Maven automatically includes:
- spring-boot-starter
- spring-boot
- spring-core
- spring-web
- spring-webmvc
- tomcat-embed-core
- jackson-databind
- ... and many more
-->View dependency tree:
# Maven
mvn dependency:tree
# Gradle
./gradlew dependenciesVersion Conflicts
When multiple dependencies require different versions of the same library, you get a version conflict.
Maven's Resolution Strategy:
- Nearest Definition: Uses the version closest to your project in the dependency tree
- First Declaration: If equidistant, uses the first declared version
Gradle's Resolution Strategy:
- Highest Version: By default, uses the highest version
Force a Specific Version (Maven):
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
</dependencies>
</dependencyManagement>Force a Specific Version (Gradle):
configurations.all {
resolutionStrategy {
force 'com.google.guava:guava:32.1.3-jre'
}
}Dependency Exclusions
Exclude transitive dependencies you don't want:
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use Jetty instead -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>Gradle:
dependencies {
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
implementation 'org.springframework.boot:spring-boot-starter-jetty'
}Bill of Materials (BOM)
BOMs manage versions for related dependencies:
Maven:
<dependencyManagement>
<dependencies>
<!-- Spring Boot BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.2.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Version inherited from BOM -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>Gradle:
dependencies {
// Import BOM
implementation platform('org.springframework.boot:spring-boot-dependencies:3.2.1')
// Version inherited from BOM
implementation 'org.springframework.boot:spring-boot-starter-web'
}Multi-Module Projects
Large projects are often split into modules for better organization.
Maven Multi-Module Project
Project Structure:
my-app/
├── pom.xml # Parent POM
├── my-app-core/
│ └── pom.xml
├── my-app-api/
│ └── pom.xml
└── my-app-web/
└── pom.xmlParent POM (pom.xml):
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.chanhle</groupId>
<artifactId>my-app-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<!-- Modules -->
<modules>
<module>my-app-core</module>
<module>my-app-api</module>
<module>my-app-web</module>
</modules>
<!-- Common Properties -->
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<!-- Dependency Management for Children -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>Child Module POM (my-app-api/pom.xml):
<project>
<modelVersion>4.0.0</modelVersion>
<!-- Parent Reference -->
<parent>
<groupId>com.chanhle</groupId>
<artifactId>my-app-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>my-app-api</artifactId>
<dependencies>
<!-- Internal Dependency -->
<dependency>
<groupId>com.chanhle</groupId>
<artifactId>my-app-core</artifactId>
<version>${project.version}</version>
</dependency>
<!-- External Dependencies (version from parent) -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>Build all modules:
mvn clean install # From parent directory
mvn clean install -pl my-app-api # Build specific moduleGradle Multi-Module Project
Project Structure:
my-app/
├── build.gradle
├── settings.gradle # Declares subprojects
├── my-app-core/
│ └── build.gradle
├── my-app-api/
│ └── build.gradle
└── my-app-web/
└── build.gradlesettings.gradle:
rootProject.name = 'my-app'
include 'my-app-core', 'my-app-api', 'my-app-web'Root build.gradle:
plugins {
id 'java' apply false
}
subprojects {
apply plugin: 'java'
group = 'com.chanhle'
version = '1.0.0-SNAPSHOT'
repositories {
mavenCentral()
}
java {
sourceCompatibility = JavaVersion.VERSION_21
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
}
test {
useJUnitPlatform()
}
}Subproject build.gradle (my-app-api/build.gradle):
dependencies {
// Internal dependency
implementation project(':my-app-core')
// External dependencies
implementation 'org.springframework.boot:spring-boot-starter-web:3.2.1'
}Java 9+ Module System (JPMS)
The Java Platform Module System (JPMS) provides stronger encapsulation and explicit dependencies.
module-info.java
Each module has a module-info.java file at the source root:
Project Structure:
my-app/
└── src/main/java/
├── module-info.java # Module descriptor
└── com/chanhle/myapp/
└── Main.javaBasic module-info.java:
module com.chanhle.myapp {
// Modules this module depends on
requires java.base; // Implicit, all modules require this
requires java.sql; // Access JDBC
requires com.google.gson; // Third-party library
// Packages this module exports (makes public)
exports com.chanhle.myapp;
exports com.chanhle.myapp.api;
// Packages exported only to specific modules
exports com.chanhle.myapp.internal to com.chanhle.myapp.web;
// Open packages for reflection (e.g., for frameworks)
opens com.chanhle.myapp.model to com.fasterxml.jackson.databind;
// Service provider interface
uses com.chanhle.myapp.spi.PluginService;
// Service implementation
provides com.chanhle.myapp.spi.PluginService
with com.chanhle.myapp.impl.DefaultPluginService;
}Module Directives
| Directive | Description |
|---|---|
requires | Declares dependency on another module |
requires transitive | Transitive dependency (callers also get access) |
requires static | Optional dependency (compile-time only) |
exports | Makes package public to all modules |
exports...to | Qualified export (only to specific modules) |
opens | Allows reflection access |
opens...to | Qualified opens |
uses | Declares service consumer |
provides...with | Declares service provider |
Automatic Modules
Non-modular JARs on the module path become "automatic modules":
module com.chanhle.myapp {
// Reference automatic module by JAR name
requires commons.lang3; // commons-lang3-3.12.0.jar → commons.lang3
}Best Practice: Use modular JARs when available.
Module Example: Spring Boot
module-info.java for Spring Boot app:
module com.chanhle.myapp {
requires spring.boot;
requires spring.boot.autoconfigure;
requires spring.web;
requires spring.context;
// Open package for Spring's component scanning
opens com.chanhle.myapp to spring.core, spring.beans, spring.context;
opens com.chanhle.myapp.controller to spring.core, spring.beans, spring.web;
// Export API packages
exports com.chanhle.myapp.api;
}Note: Many frameworks (Spring, Hibernate) use reflection, requiring opens directives.
Maven vs Gradle: Comparison
| Feature | Maven | Gradle |
|---|---|---|
| Configuration | XML (verbose) | Groovy/Kotlin (concise) |
| Performance | Slower | Faster (incremental builds, caching) |
| Flexibility | Convention-based | Highly flexible |
| Learning Curve | Easier for beginners | Steeper initially |
| Build Speed | Baseline | 2-10x faster |
| Incremental Builds | Limited | Excellent |
| Dependency Cache | Local only | Local + remote |
| Multi-Module | Good | Excellent |
| Android | Not supported | Default |
| Enterprise Adoption | Very high | Growing rapidly |
| IDE Support | Excellent | Excellent |
When to Use Maven
- Team Familiarity: Team knows Maven well
- Simple Projects: Standard Java applications
- Corporate Standards: Organization mandates Maven
- Mature Ecosystem: Extensive plugin library
When to Use Gradle
- Performance Critical: Large codebases needing fast builds
- Flexibility Needed: Complex build logic
- Multi-Language: Projects mixing Java, Kotlin, Groovy
- Android Development: Required for Android
- Modern Approach: Starting new greenfield projects
Spring Boot Integration
Both Maven and Gradle have excellent Spring Boot support.
Spring Boot with Maven
pom.xml:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>Commands:
mvn spring-boot:run # Run application
mvn package # Create executable JAR
java -jar target/my-app-1.0.0-SNAPSHOT.jar # Run JARSpring Boot with Gradle
build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.1'
id 'io.spring.dependency-management' version '1.1.4'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}Commands:
./gradlew bootRun # Run application
./gradlew bootJar # Create executable JAR
java -jar build/libs/my-app-1.0.0-SNAPSHOT.jar # Run JARProfessional Project Setup
Let's create a professional Java project from scratch with best practices.
1. Initialize with Maven
mvn archetype:generate \
-DgroupId=com.chanhle \
-DartifactId=my-app \
-DarchetypeArtifactId=maven-archetype-quickstart \
-DarchetypeVersion=1.4 \
-DinteractiveMode=false
cd my-app2. Initialize with Gradle
mkdir my-app && cd my-app
gradle init --type java-application --dsl kotlin --test-framework junit-jupiter3. Professional pom.xml Template
<?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
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.chanhle</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>My Professional App</name>
<description>Production-ready Java application</description>
<url>https://github.com/yourusername/my-app</url>
<properties>
<!-- Java Version -->
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Dependency Versions -->
<spring.boot.version>3.2.1</spring.boot.version>
<lombok.version>1.18.30</lombok.version>
<junit.version>5.10.1</junit.version>
<mockito.version>5.8.0</mockito.version>
<assertj.version>3.24.2</assertj.version>
<!-- Plugin Versions -->
<maven.compiler.plugin.version>3.11.0</maven.compiler.plugin.version>
<maven.surefire.plugin.version>3.2.3</maven.surefire.plugin.version>
<jacoco.version>0.8.11</jacoco.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
</configuration>
</plugin>
<!-- Testing -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven.surefire.plugin.version}</version>
</plugin>
<!-- Code Coverage -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- Spring Boot Plugin -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>4. Professional build.gradle.kts Template
plugins {
java
id("org.springframework.boot") version "3.2.1"
id("io.spring.dependency-management") version "1.1.4"
jacoco
}
group = "com.chanhle"
version = "1.0.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_21
}
configurations {
compileOnly {
extendsFrom(configurations.annotationProcessor.get())
}
}
repositories {
mavenCentral()
}
dependencies {
// Spring Boot
implementation("org.springframework.boot:spring-boot-starter-web")
// Lombok
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
// Testing
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.assertj:assertj-core:3.24.2")
}
tasks.test {
useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestReport {
dependsOn(tasks.test)
reports {
xml.required.set(true)
html.required.set(true)
}
}
tasks.bootJar {
archiveFileName.set("${project.name}-${project.version}.jar")
}Best Practices
1. Version Management
Use Properties/Variables:
<!-- Maven -->
<properties>
<spring.version>6.1.2</spring.version>
</properties>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>// Gradle
ext {
springVersion = '6.1.2'
}
dependencies {
implementation "org.springframework:spring-core:$springVersion"
}2. Dependency Locking
Maven Versions Plugin:
mvn versions:display-dependency-updates
mvn versions:use-latest-releasesGradle Dependency Locking:
./gradlew dependencies --write-locks3. Reproducible Builds
- Pin all versions (no
LATEST,RELEASE, or version ranges) - Use dependency lock files
- Commit lock files to version control
- Use Gradle wrapper (
gradlew) or Maven wrapper
4. Multi-Module Organization
Typical enterprise structure:
my-app/
├── my-app-domain/ # Domain models
├── my-app-service/ # Business logic
├── my-app-repository/ # Data access
├── my-app-api/ # REST controllers
└── my-app-web/ # Web application5. Avoid Dependency Hell
- Use BOM for version alignment
- Exclude transitive dependencies causing conflicts
- Review dependency tree regularly
- Keep dependencies updated (security patches)
6. Repository Management
Use a private repository (Nexus, Artifactory) for:
- Faster builds (local cache)
- Security (scan dependencies)
- Control (approved libraries only)
- Reliability (mirrors of Maven Central)
Common Troubleshooting
Maven: Dependency Not Found
# Clear local cache
rm -rf ~/.m2/repository
# Force update
mvn clean install -UGradle: Build Cache Issues
# Clean build cache
./gradlew clean --no-build-cache
# Clear global cache
rm -rf ~/.gradle/cachesVersion Conflicts
# Maven: Find conflicting versions
mvn dependency:tree -Dverbose
# Gradle: Find conflicting versions
./gradlew dependencyInsight --dependency guavaNext Steps
You now have a solid foundation in Java build tools! To continue your journey:
- Practice: Set up a multi-module Spring Boot project
- Explore Plugins: Maven Shade, Gradle Shadow for fat JARs
- CI/CD Integration: GitHub Actions, Jenkins with Maven/Gradle
- Learn Testing: Unit tests, integration tests, coverage reports
- Dependency Security: OWASP Dependency Check, Snyk
In the next post, we'll dive into Java Testing with JUnit 5, Mockito, and AssertJ.
Part of the Java Learning Roadmap series
Back to: Phase 3: Core Java APIs
Related Deep Dives:
- Java Collections Framework
- Streams & Functional Programming
- Exception Handling Best Practices
- Java Generics Explained
Next Step: Getting Started with Spring Boot
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.