Build Tools #

Imagine having to compile 50 Java files one by one from the command line, then manually download every required library, then package them into a single JAR file with the correct classpath order. That’s tedious, error-prone work that can’t be repeated consistently across machines. A build tool automates all of it. It handles compilation, dependency management, testing, and packaging — you just define what you want, and the build tool figures out how. In the Java ecosystem, there are three main build tools: Maven, Gradle, and Ant. This article covers how each works, configuration, project structure, important commands, and a guide to choosing between them.

Overview #

Each build tool has a different philosophy and approach:

AspectMavenGradleAnt
Configurationpom.xml (XML)build.gradle (Groovy/Kotlin)build.xml (XML)
PhilosophyConvention over configurationFlexible, task-basedManual and explicit
Dependency management✓ Automatic✓ Automatic✗ Manual
Build speedMediumFast (incremental + cache)Medium
Learning curveMediumMedium-highLow
EcosystemVery broadBroadLimited
Used inSpring Boot, enterpriseAndroid, modern projectsLegacy projects
flowchart LR
    A["Source Code\n(.java)"] --> B["Build Tool"]
    C["Dependencies\n(JAR)"] --> B
    D["Resources\n(config, assets)"] --> B
    B --> E["Compile\n(.class)"]
    E --> F["Test"]
    F --> G["Package\n(.jar / .war)"]
    G --> H["Deploy"]

Apache Maven #

Maven is the most popular build tool in the enterprise Java ecosystem. Its main philosophy is convention over configuration — as long as you follow Maven’s standard directory structure, almost no configuration needs to be written. Maven is also the de facto standard for publishing libraries to Maven Central, the largest public repository for the JVM ecosystem.

Project Initialization #

# Create a new project from the command line
mvn archetype:generate \
  -DgroupId=com.example \
  -DartifactId=project-name \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DinteractiveMode=false

# Parameter explanation:
# groupId     → organization identity (reversed domain, com.example)
# artifactId  → project name (project-name)
# archetype   → project template — quickstart is the most basic Java template

The result is a ready-to-use project with the standard directory structure and a pre-filled pom.xml.

Directory Structure #

project-name/
├── pom.xml                          ← the main configuration file
└── src/
    ├── main/
    │   ├── java/
    │   │   └── com/example/
    │   │       └── App.java         ← main source code
    │   └── resources/
    │       └── application.properties
    └── test/
        ├── java/
        │   └── com/example/
        │       └── AppTest.java     ← test code
        └── resources/
            └── test-config.properties

The target/ directory is created automatically when the build runs — it contains .class files, the built JAR, and test reports.

pom.xml Anatomy #

pom.xml (Project Object Model) is the heart of a Maven project. All configuration — project identity, dependencies, plugins, and the build process — is defined here.

<?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 identity -->
    <groupId>com.example</groupId>
    <artifactId>project-name</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- Global properties -->
    <properties>
        <java.version>21</java.version>
        <maven.compiler.source>${java.version}</maven.compiler.source>
        <maven.compiler.target>${java.version}</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- Dependencies the project needs -->
    <dependencies>

        <!-- Main library -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>33.0.0-jre</version>
        </dependency>

        <!-- Testing-only library — doesn't go into the production JAR -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.10.0</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <!-- Build plugins -->
    <build>
        <plugins>
            <!-- Plugin for running unit tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.1.2</version>
            </plugin>
        </plugins>
    </build>

</project>

Dependency Scopes #

The scope determines when a dependency is available and whether it’s included in the final build result.

ScopeAvailable duringIncluded in the JAR
compile (default)Compile + runtime + test
testTest only
providedCompile + test (provided by the server)
runtimeRuntime + test (not at compile time)
optionalOptional for users of this library

Important Maven Commands #

# Compile the source code
mvn compile

# Run all unit tests
mvn test

# Build the JAR file (includes compile and test)
mvn package

# Install to the local repository (~/.m2)
mvn install

# Clean the target/ directory
mvn clean

# The most commonly used combination
mvn clean package

# Skip tests (for fast builds, not recommended in CI)
mvn clean package -DskipTests

# Show the dependency tree — useful for debugging version conflicts
mvn dependency:tree

# Update dependency versions to the latest
mvn versions:display-dependency-updates

Maven Lifecycle #

Maven has three built-in lifecycles, each consisting of phases that run in sequence. When you run mvn package, all the preceding phases (validate, compile, test) are automatically executed too.

flowchart LR
    A[validate] --> B[compile] --> C[test] --> D[package] --> E[verify] --> F[install] --> G[deploy]

Gradle #

Gradle is a modern build tool that combines Maven’s strengths (automatic dependency management, public repositories) with Ant’s flexibility (build logic can be written as real code, not just XML). Gradle uses Groovy or Kotlin DSL for configuration — meaning build configuration is actual code, not just XML declarations.

Gradle’s main technical advantages are incremental builds and build caching — Gradle only recompiles files that actually changed, and can reuse caches from previous builds. This makes Gradle builds much faster than Maven on large projects.

Project Initialization #

# Interactive project initialization
gradle init

# Gradle will ask:
# - Project type: application, library, Gradle plugin
# - Language: Java, Kotlin, Groovy, Scala
# - Build script DSL: Groovy or Kotlin
# - Project name and package

# Result: a ready-to-use project with the Gradle Wrapper (./gradlew)

The Gradle Wrapper (./gradlew) is a script that downloads and runs a specific Gradle version without needing Gradle installed on the machine. This guarantees all developers and CI/CD use the same version.

Directory Structure #

project-name/
├── build.gradle          ← build configuration (Groovy DSL)
├── build.gradle.kts      ← build configuration (Kotlin DSL) — only one of them
├── settings.gradle       ← project name and multi-project configuration
├── gradlew               ← Gradle Wrapper script (Linux/Mac)
├── gradlew.bat           ← Gradle Wrapper script (Windows)
├── gradle/
│   └── wrapper/
│       └── gradle-wrapper.properties  ← the Gradle version used
└── src/
    ├── main/
    │   ├── java/
    │   │   └── com/example/
    │   │       └── App.java
    │   └── resources/
    └── test/
        ├── java/
        │   └── com/example/
        │       └── AppTest.java
        └── resources/

build.gradle Anatomy (Groovy DSL) #

// Plugin declarations — 'java' adds compile, test, jar tasks, etc.
plugins {
    id 'java'
    id 'application'
}

// Project identity
group = 'com.example'
version = '1.0.0'

// Java configuration
java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

// Repositories for downloading dependencies
repositories {
    mavenCentral() // Maven Central — the main public repository
}

// Project dependencies
dependencies {
    // Main dependency (compile + runtime)
    implementation 'com.google.guava:guava:33.0.0-jre'

    // Test-only dependency
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

// Test task configuration
test {
    useJUnitPlatform()
}

// Main class for the 'run' task
application {
    mainClass = 'com.example.App'
}

build.gradle.kts Anatomy (Kotlin DSL) #

The Kotlin DSL is growing in popularity because it provides better type safety and autocomplete in IDEs.

plugins {
    java
    application
}

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

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.google.guava:guava:33.0.0-jre")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

tasks.test {
    useJUnitPlatform()
}

application {
    mainClass = "com.example.App"
}

Dependency Scope Configuration #

Gradle uses different configuration names from Maven:

GradleMaven scope equivalentDescription
implementationcompileMain dependency, doesn’t leak to consumers
apicompileMain dependency, leaks to consumers (for libraries)
testImplementationtestTest only
compileOnlyprovidedCompile time only, not runtime
runtimeOnlyruntimeRuntime only, not at compile time

Important Gradle Commands #

# Use the Gradle Wrapper (always use this, not gradle directly)
./gradlew <task>    # Linux/Mac
gradlew.bat <task>  # Windows

# Main tasks
./gradlew build          # compile + test + jar
./gradlew test           # run all tests
./gradlew run            # run the main class (needs the 'application' plugin)
./gradlew jar            # build the JAR file without tests
./gradlew clean          # delete the build/ directory
./gradlew clean build    # clean then rebuild

# Skip tests
./gradlew build -x test

# List all available tasks
./gradlew tasks

# Show the dependency tree
./gradlew dependencies

# Show dependencies for a specific configuration
./gradlew dependencies --configuration compileClasspath

# Build with detailed information
./gradlew build --info

# Build with a performance scan (needs an internet connection)
./gradlew build --scan

Custom Tasks #

One of Gradle’s strengths is the ability to define custom tasks directly in build.gradle.

// A simple custom task
tasks.register('greet') {
    doLast {
        println "Hello from Gradle!"
    }
}

// A task that depends on another task
tasks.register('deployJar', Copy) {
    dependsOn jar
    from jar.archiveFile
    into '/opt/app/deploy/'
}

// Run it: ./gradlew greet

Apache Ant #

Ant is the oldest of the three build tools — it existed long before Maven and Gradle. Unlike both, Ant has no opinion about how a project should be structured and doesn’t handle dependency management automatically. You define every build step explicitly in build.xml. This flexibility is useful in certain scenarios, but it also means more manual work.

Directory Structure #

Ant doesn’t enforce a particular structure, but the most common convention is:

project-name/
├── build.xml             ← the main build script
├── src/
│   └── com/example/
│       └── App.java
├── test/
│   └── com/example/
│       └── AppTest.java
├── lib/
│   └── junit-5.10.0.jar  ← dependencies stored manually
├── build/
│   ├── classes/          ← compilation results
│   └── test-classes/
└── dist/
    └── project-name.jar   ← the final build result

build.xml Anatomy #

<?xml version="1.0" encoding="UTF-8"?>
<project name="ProjectName" default="jar" basedir=".">

    <!-- Properties — variables usable throughout the file -->
    <property name="src.dir"          value="src"/>
    <property name="test.dir"         value="test"/>
    <property name="lib.dir"          value="lib"/>
    <property name="build.dir"        value="build"/>
    <property name="build.classes"    value="${build.dir}/classes"/>
    <property name="build.test"       value="${build.dir}/test-classes"/>
    <property name="dist.dir"         value="dist"/>
    <property name="jar.name"         value="project-name.jar"/>

    <!-- Classpath: required libraries -->
    <path id="compile.classpath">
        <fileset dir="${lib.dir}" includes="*.jar"/>
    </path>

    <path id="test.classpath">
        <path refid="compile.classpath"/>
        <pathelement location="${build.classes}"/>
        <pathelement location="${build.test}"/>
    </path>

    <!-- Target: init — create the required directories -->
    <target name="init">
        <mkdir dir="${build.classes}"/>
        <mkdir dir="${build.test}"/>
        <mkdir dir="${dist.dir}"/>
    </target>

    <!-- Target: compile — compile the source code -->
    <target name="compile" depends="init">
        <javac srcdir="${src.dir}"
               destdir="${build.classes}"
               classpathref="compile.classpath"
               includeantruntime="false"
               source="21" target="21"
               encoding="UTF-8"/>
    </target>

    <!-- Target: compile-test — compile the test code -->
    <target name="compile-test" depends="compile">
        <javac srcdir="${test.dir}"
               destdir="${build.test}"
               classpathref="test.classpath"
               includeantruntime="false"
               source="21" target="21"/>
    </target>

    <!-- Target: test — run unit tests -->
    <target name="test" depends="compile-test">
        <junit printsummary="yes" haltonfailure="yes">
            <classpath refid="test.classpath"/>
            <batchtest>
                <fileset dir="${build.test}" includes="**/*Test.class"/>
            </batchtest>
        </junit>
    </target>

    <!-- Target: jar — build the JAR file -->
    <target name="jar" depends="compile">
        <jar destfile="${dist.dir}/${jar.name}"
             basedir="${build.classes}">
            <manifest>
                <attribute name="Main-Class" value="com.example.App"/>
            </manifest>
        </jar>
        <echo>JAR created: ${dist.dir}/${jar.name}</echo>
    </target>

    <!-- Target: clean — delete the build results -->
    <target name="clean">
        <delete dir="${build.dir}"/>
        <delete dir="${dist.dir}"/>
    </target>

</project>

Running Ant Targets #

# Run the default target (per the default attribute in <project>)
ant

# Run a specific target
ant compile
ant test
ant jar
ant clean

# Run several targets in sequence
ant clean jar

# Use a different build file
ant -f build-prod.xml jar

# List all available targets
ant -projecthelp

Ivy — Dependency Management for Ant #

Ant itself has no dependency management. For that, you can add Apache Ivy — a separate library that gives Ant projects automatic dependency downloading.

<!-- Add to build.xml -->
<taskdef resource="org/apache/ivy/ant/antlib.xml"
         uri="antlib:org.apache.ivy.ant"
         classpath="lib/ivy.jar"/>

<!-- Resolve dependencies based on ivy.xml -->
<target name="resolve">
    <ivy:retrieve/>
</target>
<!-- ivy.xml: dependency list, similar to Maven's pom.xml -->
<ivy-module version="2.0">
    <info organisation="com.example" module="project-name"/>
    <dependencies>
        <dependency org="com.google.guava" name="guava" rev="33.0.0-jre"/>
        <dependency org="org.junit.jupiter" name="junit-jupiter" rev="5.10.0" conf="test->default"/>
    </dependencies>
</ivy-module>

Configuration File Comparison #

To add the same dependency — say Guava — here’s how it’s written in all three build tools:

Adding the Guava Dependency #

Maven (pom.xml):

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.0.0-jre</version>
</dependency>

Gradle Groovy (build.gradle):

implementation 'com.google.guava:guava:33.0.0-jre'

Gradle Kotlin (build.gradle.kts):

implementation("com.google.guava:guava:33.0.0-jre")

Ant + Ivy (ivy.xml):

<dependency org="com.google.guava" name="guava" rev="33.0.0-jre"/>

Ant without Ivy:

Manually download guava-33.0.0-jre.jar → save to lib/ → reference it in the classpath

IDE Integration #

IntelliJ IDEA #

IntelliJ detects build tools automatically when you open a project. You can import directly and the IDE downloads dependencies and configures the classpath without extra steps.

Maven → open the project folder containing pom.xml → Import as Maven Project
Gradle → open the project folder containing build.gradle → Import as Gradle Project
Ant   → open the project folder → add build.xml via the Ant tool window menu

VS Code #

# Install the Extension Pack for Java (includes Maven and Gradle support)
# Maven: use the "Maven for Java" extension
# Gradle: use the "Gradle for Java" extension

When to Use Each Build Tool #

Use MAVEN when:
  ✓ Enterprise Java or Spring Boot projects
  ✓ The team is already familiar with Maven and its ecosystem
  ✓ You need strict, easy-to-understand standard conventions
  ✓ You'll publish libraries to Maven Central
  ✓ Multi-module projects with a simple hierarchical structure

Use GRADLE when:
  ✓ Android projects (Gradle is the only official option)
  ✓ You need fast builds — incremental builds and caching really help
  ✓ Complex build logic that can't be expressed with XML alone
  ✓ Multi-project builds with complicated inter-module dependencies
  ✓ The team is comfortable writing configuration as code (Groovy/Kotlin)

Use ANT when:
  ✓ Maintaining old projects that already use Ant
  ✓ You need full control over every build step
  ✓ Integration with non-standard toolchains unsupported by Maven/Gradle
  ✗ Avoid it for new projects — choose Maven or Gradle

Default choice for new projects:
  → Maven if it's an enterprise team needing simplicity
  → Gradle if you need high performance or flexibility

Summary #

  • Maven uses pom.xml (XML) with the convention over configuration philosophy. The src/main/java and src/test/java directory structure is already an industry standard in Java. Good for enterprise projects and Spring Boot.
  • Gradle uses build.gradle (Groovy or Kotlin DSL), which is more concise and expressive. Incremental builds and build caching make it much faster than Maven on large projects. Mandatory for Android.
  • Ant uses build.xml and provides full control but without built-in dependency management. Still relevant for legacy projects and highly custom build scenarios.
  • Gradle Wrapper (./gradlew) is the best way to run Gradle — it ensures all developers and CI/CD use the same Gradle version without manual installation.
  • Dependency scopes matter for efficiency — mark testing libraries with scope test (Maven) or testImplementation (Gradle) so they don’t go into the production build.
  • mvn clean package and ./gradlew clean build are the most commonly used clean build commands — delete old results, compile, test, then package.
  • The Maven Lifecycle runs sequentially — running mvn package automatically runs validate, compile, and test before it.
  • Ant without Ivy means manual dependency downloads — consider adding Ivy or migrating to Maven/Gradle if dependency management becomes a burden.

← Previous: Regex   Next: Multi Threading →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact