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.
<?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.
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.
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
In this article we have shown how to use Maven for build automation and dependency management in Java projects.
Author
List all Java tutorials.