ZetCode

Maven introduction

last modified November 8, 2026

In this article we show how to use Maven for build automation and dependency management in Java projects. The examples use Maven 3.10 and Java 25.

What is Maven?

Maven is a build automation and project management tool used mainly for Java projects. It provides a standard directory layout, downloads the libraries a project depends on, and builds the project with a fixed sequence of steps called a lifecycle.

Maven is declarative: we describe what the project is and what it needs, not how to build it. The description lives in an XML file called pom.xml (Project Object Model). The actual work, such as compiling, testing, and packaging, is done by plugins.

Key benefits of Maven include automatic dependency management, a standard project structure that every Java developer and IDE understands, and a large ecosystem of plugins for testing, packaging, and deployment.

Installation

Maven is a Java program, so a JDK must be installed first. Maven 3.10 runs on Java 8 or later; the Java version that compiles our code is configured separately in the POM.

On Windows, download the binary archive from https://maven.apache.org/download.cgi, extract it, for example to C:\Program Files\Apache\maven, and add its bin directory to the PATH environment variable. The JAVA_HOME variable must point to the JDK. The old M2_HOME variable is no longer used.

Linux distributions and Homebrew ship Maven packages, but these often lag behind the latest release. SDKMAN installs the current version on Linux and macOS and can switch between several versions.

$ sdk install maven       # SDKMAN (Linux, macOS)
$ brew install maven      # Homebrew (macOS)
$ sudo apt install maven  # Debian, Ubuntu

Verifying the installation

The --version option prints the Maven version together with the Java runtime Maven runs on.

$ mvn --version
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)
Maven home: /home/user/.sdkman/candidates/maven/current
Java version: 25.0.3, vendor: Amazon.com Inc., runtime: /home/user/.sdkman/candidates/java/25.0.3-amzn
Default locale: en, platform encoding: UTF-8, time zone: Europe/Bratislava
OS name: "linux", version: "6.8.0-138-generic", arch: "amd64", family: "unix"

Basic concepts

The pom.xml file is the heart of every Maven project. It contains the project coordinates, the dependencies, and the build configuration. Maven combines it with built-in defaults (the Super POM) to get the effective POM, which can be printed with mvn help:effective-pom.

Every project and every library is identified by its coordinates: the groupId, usually a reversed domain name of the organization, the artifactId, the name of the project, and the version. A version ending in -SNAPSHOT marks a development version that is still changing.

Maven expects a standard directory layout. Application sources go to src/main/java, resources such as configuration files to src/main/resources, and tests to src/test/java. Everything Maven produces goes to the target directory.

A build is driven by lifecycles. The default lifecycle builds the project, the clean lifecycle removes the build output, and the site lifecycle generates documentation. Each lifecycle consists of phases such as compile, test, and package; running a phase runs all phases before it.

Creating a project

The Archetype plugin generates new projects from templates called archetypes. The maven-archetype-quickstart archetype creates a simple Java application.

The -DarchetypeVersion option matters: without it, the plugin falls back to version 1.0 of the quickstart archetype, an outdated template with JUnit 3. With -DinteractiveMode=false, Maven does not ask questions and uses the given values.

$ mvn archetype:generate -DgroupId=com.example -DartifactId=myapp \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DarchetypeVersion=1.5 -DinteractiveMode=false
...
[INFO] Using following parameters for creating project from Archetype: maven-archetype-quickstart:1.5
...
[INFO] Parameter: junitVersion, Value: 5.11.0
...
[INFO] Parameter: javaCompilerVersion, Value: 17
...
[INFO] Project created from Archetype in dir: /home/user/myapp
...
[INFO] BUILD SUCCESS
...

Project structure

The archetype created the standard directory layout with one application class and one test class. The empty .mvn directory holds optional per-project Maven settings: maven.config for command line options and jvm.config for JVM options.

$ tree -a myapp
myapp
├── .mvn
│   ├── jvm.config
│   └── maven.config
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── com
    │           └── example
    │               └── App.java
    └── test
        └── java
            └── com
                └── example
                    └── AppTest.java

10 directories, 5 files

Understanding the POM

The generated pom.xml targets Java 17 and JUnit 5. We update it for Java 25 and JUnit 6 and keep only the parts we need.

pom.xml
<?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>

    <groupId>com.example</groupId>
    <artifactId>myapp</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.release>25</maven.compiler.release>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit</groupId>
                <artifactId>junit-bom</artifactId>
                <version>6.1.3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.16.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>3.6.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-jar-plugin</artifactId>
                    <version>3.5.1</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

The POM defines the project coordinates, the Java version, the test dependency, and the versions of the plugins Maven uses to build the project.

<modelVersion>4.0.0</modelVersion>

The version of the POM model. It is 4.0.0 for both Maven 3 and Maven 4.

<groupId>com.example</groupId>
<artifactId>myapp</artifactId>
<version>1.0-SNAPSHOT</version>

The project coordinates. Without a packaging element, the project is packaged as a JAR file.

<maven.compiler.release>25</maven.compiler.release>

The maven.compiler.release property tells the compiler plugin to compile for Java 25. The release option checks both the language level and the JDK API, so the code cannot accidentally use classes that do not exist in the target version. Since Maven 3.10, the source encoding defaults to UTF-8, so the project.build.sourceEncoding property is no longer needed.

<dependency>
    <groupId>org.junit</groupId>
    <artifactId>junit-bom</artifactId>
    <version>6.1.3</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

The JUnit BOM (bill of materials) in the dependencyManagement section manages the versions of all JUnit artifacts. Therefore, the junit-jupiter dependency does not need a version.

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

The test scope means that JUnit is available only when compiling and running tests; it does not become part of the application.

<pluginManagement>

The pluginManagement section pins the versions of the compiler, Surefire (test runner), and JAR plugins. Without it, the build depends on the default versions built into the Maven release that happens to run it.

The application

We replace the generated hello world class with a slightly bigger one.

src/main/java/com/example/App.java
package com.example;

public class App {

    static String greet(String name) {
        return "Hello %s!".formatted(name);
    }

    void main() {
        IO.println(greet("Maven"));
        IO.println("Java " + Runtime.version().feature());
    }
}

Since Java 25, the main method does not have to be public static, and the java.lang.IO class provides simple console output. With the generated release 17, the compiler stops with a cannot find symbol error, because the IO class does not exist in the Java 17 API.

src/test/java/com/example/AppTest.java
package com.example;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

class AppTest {

    @Test
    void greetsByName() {
        assertEquals("Hello Maven!", App.greet("Maven"));
    }
}

The test checks the greet method. JUnit test classes and methods do not need to be public; package-private visibility is enough.

Building the project

The package phase compiles the sources, runs the tests, and creates the JAR file. Each line starting with --- shows a plugin goal run by a phase: for instance, compiler:3.16.0:compile is the compile goal of the compiler plugin, version 3.16.0. The resources plugin is not pinned in our POM, so Maven 3.10 uses its built-in default version.

$ mvn package
[INFO] Scanning for projects...
[INFO] Loaded 24410 auto-discovered prefixes for remote repository central (prefixes-central.txt)
[INFO] 
[INFO] -------------------------< com.example:myapp >--------------------------
[INFO] Building myapp 1.0-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- resources:3.5.0:resources (default-resources) @ myapp ---
[INFO] skip non existing resourceDirectory /home/user/modern/src/main/resources
[INFO] 
[INFO] --- compiler:3.16.0:compile (default-compile) @ myapp ---
[INFO] Loaded 64 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt)
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 1 source file with javac [debug release 25] to target/classes
[INFO] 
[INFO] --- resources:3.5.0:testResources (default-testResources) @ myapp ---
[INFO] skip non existing resourceDirectory /home/user/modern/src/test/resources
[INFO] 
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ myapp ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 1 source file with javac [debug release 25] to target/test-classes
[INFO] 
[INFO] --- surefire:3.6.1:test (default-test) @ myapp ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO] 
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.example.AppTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.040 s -- in com.example.AppTest
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] 
[INFO] 
[INFO] --- jar:3.5.1:jar (default-jar) @ myapp ---
[INFO] Building jar: /home/user/modern/target/myapp-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.284 s
[INFO] Finished at: 2026-11-08T21:01:54+01:00
[INFO] ------------------------------------------------------------------------

Running the application

The JAR file contains the compiled classes. We run the App class with the java command; the -cp option puts the JAR on the classpath.

$ java -cp target/myapp-1.0-SNAPSHOT.jar com.example.App
Hello Maven!
Java 25

Common Maven commands

These commands cover most daily work. Phases from different lifecycles can be combined, for example mvn clean package removes the old build output first and then builds the project from scratch. The install phase copies the artifact to the local repository in ~/.m2/repository, where other projects on the same machine can use it as a dependency.

$ mvn clean            # delete the target directory
$ mvn compile          # compile the sources
$ mvn test             # compile and run the tests
$ mvn package          # build the JAR file
$ mvn install          # copy the JAR to the local repository
$ mvn dependency:tree  # show the dependency tree

Source

Maven guides

In this article we have shown how to use Maven for build automation and dependency management in Java projects.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Java tutorials.