Back to blog

Deep Dive: Java Modules and Build Tools

javamavengradlebuild-toolsmodules
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:

ScopeBuildTestRuntimeDescription
compileDefault scope, available everywhere
providedProvided by runtime (e.g., servlet API)
runtimeNot needed for compilation (e.g., JDBC drivers)
testOnly for testing (e.g., JUnit)
systemLocal JAR files (avoid if possible)
importN/AN/AN/AImport 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 repository

2. Clean Lifecycle

mvn clean         # Delete target/ directory

3. Site Lifecycle

mvn site          # Generate project documentation

Phase Chaining:

Each phase executes all previous phases:

mvn package       # Runs: validate → compile → test → package
mvn clean install # Runs: clean → validate → compile → test → package → install

Maven 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 profile

Gradle: 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:

GradleMaven EquivalentDescription
implementationcompileAvailable at compile and runtime
apicompileLike implementation but transitive
compileOnlyprovidedCompile-time only
runtimeOnlyruntimeRuntime only
testImplementationtestTest compile and runtime
testCompileOnlytest (provided)Test compile only
testRuntimeOnlytest (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 JAR

Custom 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 copyResources

Gradle 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     # Windows

Initialize wrapper:

gradle wrapper --gradle-version=8.5

Dependency 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 dependencies

Version 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.xml

Parent 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 module

Gradle 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.gradle

settings.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.java

Basic 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

DirectiveDescription
requiresDeclares dependency on another module
requires transitiveTransitive dependency (callers also get access)
requires staticOptional dependency (compile-time only)
exportsMakes package public to all modules
exports...toQualified export (only to specific modules)
opensAllows reflection access
opens...toQualified opens
usesDeclares service consumer
provides...withDeclares 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

FeatureMavenGradle
ConfigurationXML (verbose)Groovy/Kotlin (concise)
PerformanceSlowerFaster (incremental builds, caching)
FlexibilityConvention-basedHighly flexible
Learning CurveEasier for beginnersSteeper initially
Build SpeedBaseline2-10x faster
Incremental BuildsLimitedExcellent
Dependency CacheLocal onlyLocal + remote
Multi-ModuleGoodExcellent
AndroidNot supportedDefault
Enterprise AdoptionVery highGrowing rapidly
IDE SupportExcellentExcellent

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 JAR

Spring 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 JAR

Professional 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-app

2. Initialize with Gradle

mkdir my-app && cd my-app
gradle init --type java-application --dsl kotlin --test-framework junit-jupiter

3. 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-releases

Gradle Dependency Locking:

./gradlew dependencies --write-locks

3. 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 application

5. 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 -U

Gradle: Build Cache Issues

# Clean build cache
./gradlew clean --no-build-cache
 
# Clear global cache
rm -rf ~/.gradle/caches

Version Conflicts

# Maven: Find conflicting versions
mvn dependency:tree -Dverbose
 
# Gradle: Find conflicting versions
./gradlew dependencyInsight --dependency guava

Next Steps

You now have a solid foundation in Java build tools! To continue your journey:

  1. Practice: Set up a multi-module Spring Boot project
  2. Explore Plugins: Maven Shade, Gradle Shadow for fat JARs
  3. CI/CD Integration: GitHub Actions, Jenkins with Maven/Gradle
  4. Learn Testing: Unit tests, integration tests, coverage reports
  5. 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:

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.