Maven pom.xml Explained: Structure, GAV, Properties, Packaging and Build
pom.xml is the XML file in the root of a Maven project that holds the POM (Project Object Model): the project coordinates (groupId, artifactId, version), the build type (packaging), properties, dependencies and build settings. Maven reads pom.xml on every run, validates its structure and merges it with the defaults; it then uses the resulting model to run the requested phases and goals: resolving dependencies, compiling the code, packaging the result and so on.
Older Maven tutorials often contain this snippet: <sourceDirectory>src</sourceDirectory>. Paste it into a project with the standard layout and Maven starts compiling the Java sources in the whole src tree as main code, including the tests in src/test/java. If the project has JUnit Jupiter tests with test scope, mvn compile then fails with package org.junit.jupiter.api does not exist: JUnit is on the test classpath only, so the main code cannot see it. One line in pom.xml breaks the build. Below we go through which elements of this file you actually need and what each of them changes.
pom.xml Structure: the Main Elements
A Maven POM file needs only the <project> root element, <modelVersion> and the coordinates groupId, artifactId and version (groupId and version can be inherited from a parent). Everything else is optional: settings you leave out may be inherited from a parent and the Super POM or come from the POM model or the plugins themselves, and an optional element may simply be absent. The default bindings of plugin goals to phases are determined by packaging.
| Element | What it defines | Required? | If omitted |
|---|---|---|---|
modelVersion | The POM format version | Yes | The POM cannot be read |
groupId, artifactId, version | The project coordinates (GAV) | Yes (groupId and version only if there is no parent) | An error such as 'groupId' is missing |
packaging | The artifact type and the set of plugins bound to build phases | No | jar |
name, description, url | Human-readable project information and the project site | No | The build header shows the artifactId instead of name; description and url can be inherited from a parent |
properties | Values for ${...} substitution and plugin settings | No | Plugins use inherited values or their own defaults (in Maven 3.9.x, for example, the platform encoding) |
dependencies | Libraries the project needs | No | No dependencies declared directly in the project (they can still come from a parent or a profile) |
build | Directories, resources, the output file name, plugins | No | Standard directories from the Super POM; plugin goal bindings from the lifecycle mapping for the packaging |
parent | The parent POM whose settings are inherited | No | The Super POM acts as the parent |
The order of elements inside <project> does not affect the result, but by convention they follow the order of the table: coordinates first, then description, properties, dependencies and build. The shortest working 12-line pom.xml is shown in the lesson What Is Maven; below, each element is explained separately, and at the end everything is put together in a full example.
The project Root Element and modelVersion
The <project> root element contains the entire project description, and the nested <modelVersion> with the value 4.0.0 tells Maven which version of the POM model the file follows. For Maven 3 this is the only valid value. The namespace attributes and xsi:schemaLocation are optional, but they let your IDE validate tags and autocomplete them.
<?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>
<!-- coordinates, properties, dependencies, build -->
</project> The xmlns value is a namespace identifier, not a download address, so there is no need to change http to https in it. Maven 4 introduces model version 4.1.0 with new features, but it still reads files with 4.0.0, so for learning and most real-world projects stick with 4.0.0.
GAV Coordinates: groupId, artifactId, version
GAV is the triple of groupId, artifactId and version that uniquely identifies a project (or any library) in the Maven world. groupId is the organization or project group written as a reversed domain name, artifactId is the project name, and version is its version. The pair groupId + artifactId without a version identifies the project as a whole, not a specific build. A specific file is further distinguished by its extension (type) and, when needed, a classifier: the main jar and the sources jar (-sources) share the same GAV.
<groupId>com.examclouds</groupId>
<artifactId>shop</artifactId>
<version>1.0-SNAPSHOT</version> The coordinates determine both the path in the repository and the file name: after mvn install, the project above ends up in ~/.m2/repository/com/examclouds/shop/1.0-SNAPSHOT/shop-1.0-SNAPSHOT.jar. How to choose the values (which characters are allowed, why it is recommended to avoid com.my-company as a groupId) is covered in How to Choose groupId, artifactId and version; the official recommendations are in the Guide to Naming Conventions.
SNAPSHOT vs release. The -SNAPSHOT suffix (1.0-SNAPSHOT) marks a version under development: its content may change. Maven looks for a newer build of a SNAPSHOT dependency in the remote repository during a build, when the repository's updatePolicy says it is due (the default is daily — at most once a day); you can force an update of SNAPSHOT dependencies with mvn -U. Maven Central itself is configured in the Super POM with snapshots disabled, so SNAPSHOT versions come from a separate (usually corporate) repository. A version without the suffix (1.0.0) is a release: a published release is treated as immutable, and on Maven Central it can be neither overwritten nor deleted. That is why a fix to a release is shipped as a new version (1.0.1).
Maven Packaging: jar, war, pom
The <packaging> element defines what artifact the build produces and therefore the default bindings of plugin goals to lifecycle phases (the lifecycle mapping, see Plugin Bindings for default Lifecycle). The element is optional: if it is missing, jar is used — the default value of the POM model itself.
| packaging | Build result | What runs in the package phase | When to use it |
|---|---|---|---|
jar (default) | target/artifactId-version.jar | maven-jar-plugin | Console applications, libraries, Spring Boot applications |
war | A .war web archive with the content of src/main/webapp | maven-war-plugin | A web application deployed to Tomcat or another servlet container |
pom | Only the pom.xml itself, no code and no jar | Nothing: no compilation or packaging | Parent POMs, aggregators of a multi-module project (the <modules> list), BOMs and other projects that publish only a POM |
maven-plugin, ear, ejb | A Maven plugin, a Jakarta EE enterprise archive, an EJB module | The corresponding plugins | Special cases |
Because of this binding, changing packaging changes the whole build: set it to pom in a project with code in src/main/java, and mvn package ends with BUILD SUCCESS without compiling a single class. The build phases and their order are covered in a separate lesson of this module on the Maven build lifecycle.
name, description and url
The <name>, <description> and <url> elements describe the project for people: a readable name, a short description and the address of the project site or repository. On their own they do not define any build steps, but plugins may use this metadata (for example, when generating the project site). name appears in Maven's output, while description and url can be inherited from a parent. All three are required when you publish a library to Maven Central.
<name>Online Shop</name>
<description>A training online shop in Java</description>
<url>https://github.com/examclouds/shop</url> When name is set, Maven shows it in the build header instead of the artifactId (without name the header reads Building shop 1.0-SNAPSHOT):
[INFO] -----------------------< com.examclouds:shop >------------------------
[INFO] Building Online Shop 1.0-SNAPSHOT
[INFO] from pom.xml Open-source libraries usually add <licenses>, <developers> and <scm> (the source code repository) next to them — more metadata that Maven Central requires.
Maven Properties and ${...} Expressions
The <properties> section stores name-value pairs that can be substituted with the expression ${name} into many POM elements, such as dependency versions and plugin configuration. Some fields have restrictions: in particular, the <parent> coordinates must be literal values in Maven 3. Maven properties serve two purposes: they remove duplication (for example, one version shared by several related libraries) and they pass parameters to plugins — the Java version and the source encoding.
<properties>
<!-- read by plugins -->
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- a custom property -->
<junit.version>5.11.4</junit.version>
</properties>
<!-- usage: -->
<version>${junit.version}</version> maven.compiler.release— the Java version formaven-compiler-plugin: language level, bytecode and JDK API. Why you needreleaserather than thesource/targetpair is shown in the lesson What Is Maven.project.build.sourceEncoding— the encoding of sources and resources for the compiler and resources plugins. SettingUTF-8explicitly keeps the build consistent across Maven versions. In Maven 3.9.x, with no encoding set or inherited, plugins may fall back to the platform encoding and print a warning (for filtered resources, depending on the plugin version,Using platform encoding (...) to copy filtered resources, i.e. build is platform dependent!), and on JDK 17 and earlier on Windows non-ASCII characters in string literals can turn into garbage. Since Maven 3.10.0,UTF-8is already set in the Super POM.- Custom properties (
junit.version) can have any name that is a valid XML element name; dots are simply part of the name, not nesting.
Besides your own properties, expressions can use built-in project model values, environment variables and Java system properties:
| Expression | What it resolves to | Example value |
|---|---|---|
${project.version} | The project version from <version> | 1.0-SNAPSHOT |
${project.groupId}, ${project.artifactId} | The project coordinates | com.examclouds, shop |
${project.basedir} | The directory containing pom.xml | /home/user/shop |
${project.build.directory} | The build output directory | /home/user/shop/target |
${project.build.finalName} | The output file name without extension | shop-1.0-SNAPSHOT |
${env.JAVA_HOME} | An environment variable (env. prefix) | /usr/lib/jvm/java-25-openjdk |
${java.version}, ${user.home} | System properties of the JVM running Maven | 25.0.1, /home/user |
A property can be overridden from the command line without editing the file: mvn package -Djunit.version=5.12.0 — a value passed with -D takes precedence over <properties>. The short forms ${pom.version} and ${version} found in old articles are deprecated: Maven 3 prints The expression ${pom.version} is deprecated. Please use ${project.version} instead., so always write the full project. prefix.
Quirk: a typo in ${...} does not always break the build
An unresolved expression does not always cause an immediate error. In a dependency version (<version>${junit.verison}</version>) it leads to a model validation error: Maven 3 rejects the string as an invalid version. In a filtered text resource the expression may simply stay unreplaced as ${junit.verison}. In plugin configuration the outcome depends on the parameter and the plugin itself: the expression may be kept, evaluated later, become an empty value or cause a plugin error. To see what the expressions in the model resolved to, check the effective POM.
The dependencies Section
The <dependencies> section lists the libraries your project needs; each one is declared with a <dependency> element using the same GAV coordinates as the project itself. Maven resolves the listed dependencies (finds them in the local repository or downloads them from a remote one) and adds them to the classpath. The optional <scope> element specifies where the library is needed: test means "only for compiling and running tests".
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies> The other scopes, transitive dependencies, version conflict resolution, exclusions and centralized version management will be covered in a separate lesson of this module on Maven dependencies.
The build Section: finalName, Directories, Resources, Plugins
The <build> section describes how the project is built: the output file name, the source and resource directories and the plugins in use. It is optional: for a project with the standard layout, all paths are already set in the Super POM, so you need <build> only to change the defaults or configure plugins.
Default directories and when to override them
| Element | Default value | What it defines |
|---|---|---|
sourceDirectory | src/main/java | Main source code (a single directory only) |
testSourceDirectory | src/test/java | Test sources |
resources / resource | src/main/resources | Resource directories copied to target/classes |
directory | target | The directory for all build output |
outputDirectory | target/classes | Where .class files and resources go |
finalName | ${project.artifactId}-${project.version} | The name of the output jar/war in target (without extension) |
Override directories only when you move an old project to Maven and cannot change its layout. In that case set both paths explicitly so that main code and tests do not overlap (otherwise you get the error from the beginning of this lesson):
<build>
<sourceDirectory>src</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
</build> For a new project, it is better to put files into the standard directories — see the table with examples in Maven Project Structure.
finalName: the name in target vs the name in the repository
<finalName> changes only the file name in the target directory. When installing into the local repository, Maven still names the file by its coordinates, artifactId-version, because other projects find it by GAV:
<build>
<finalName>shop</finalName>
</build> mvn install
# Output (fragment):
[INFO] Building jar: /home/user/shop/target/shop.jar
[INFO] Installing /home/user/shop/target/shop.jar to /home/user/.m2/repository/com/examclouds/shop/1.0-SNAPSHOT/shop-1.0-SNAPSHOT.jar finalName is most often set for a war: Tomcat derives the context path from the file name, so shop.war is served at /shop instead of /shop-1.0-SNAPSHOT.
Resources and filtering
Resources (.properties, .xml, templates) are not compiled; they are copied to target/classes and end up on the classpath. If you enable <filtering>true</filtering> for a directory, Maven replaces ${...} expressions in the files with property values while copying — one way to pass the project version to your program:
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build> # src/main/resources/app.properties
app.version=${project.version}
# target/classes/app.properties after mvn package
app.version=1.0-SNAPSHOT Not obvious: resources replaces, it does not add
The <resources> list in your pom.xml completely replaces the list from the Super POM. If you declare only <directory>src/main/config</directory>, src/main/resources is no longer copied: the build succeeds, but getClass().getResourceAsStream("/app.properties") returns null. When adding your own resource directory, list the standard one too.
plugins vs pluginManagement
All the work during a build is done by plugins. <build><plugins> lists the plugins to run or configure in this project, while <build><pluginManagement> only sets versions and configuration "in case they are used": on its own it activates nothing and is most often found in a parent POM. The standard plugins (compiler, surefire, jar) work without being declared, but you should pin their versions, otherwise they depend on your Maven version. How to declare plugins, bind their goals to phases and configure them is covered in a separate lesson of this module on Maven plugins.
The reporting section
The <reporting> section adds report plugins (Javadoc, test results, static analysis) that run only in the site lifecycle, when the project site is generated: with mvn site or other phases of that lifecycle (for example, mvn site-deploy). It takes no part in a regular build (mvn package), and modern projects usually run quality checks in <build> or on CI. The FindBugs plugin from old examples is no longer developed — it has been replaced by spotbugs-maven-plugin.
parent, Super POM and Effective POM
Every pom.xml inherits settings from a parent: either an explicit one declared in <parent> or the implicit Super POM built into Maven. The final model Maven gets after inheritance, profile activation and property interpolation is called the effective POM; the command mvn help:effective-pom prints it.
parent. A parent POM has packaging set to pom; a child project references it by GAV and inherits properties, dependencies, pluginManagement, groupId and version. artifactId is not inherited — every module has its own. This is how multi-module projects and Spring Boot (with the spring-boot-starter-parent parent) work.
<parent>
<groupId>com.examclouds</groupId>
<artifactId>shop-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- by default the parent is looked up in ../pom.xml -->
</parent>
<artifactId>shop-core</artifactId>
<!-- groupId and version are inherited --> The Super POM defines defaults for every project: the standard directories src/main/java and target, resources, finalName and the Maven Central repository. That is why a minimal pom.xml of just a few lines can already build a project. But not every default comes from the Super POM: packaging = jar is a default of the POM model itself, the bindings of compiler, surefire and jar to phases come from the lifecycle mapping for the chosen packaging, and plugins have their own default parameter values.
The effective POM is your main tool when you cannot tell where a setting comes from or why a property was not applied:
mvn help:effective-pom
# save to a file:
mvn help:effective-pom -Doutput=effective-pom.xml <!-- Output (fragment): paths are absolute, properties are resolved -->
<build>
<sourceDirectory>/home/user/shop/src/main/java</sourceDirectory>
<testSourceDirectory>/home/user/shop/src/test/java</testSourceDirectory>
<outputDirectory>/home/user/shop/target/classes</outputDirectory>
<directory>/home/user/shop/target</directory>
<finalName>shop-1.0-SNAPSHOT</finalName>
... The effective POM shows everything that is not in your file: default plugin versions, the Maven Central repository, dependencies inherited from the parent. Keep its limits in mind: it is the final model, not every actual plugin execution parameter (defaults defined inside a plugin are not shown) and not the contents of filtered files. Declaring your own repositories in <repositories> is covered in a separate lesson of this module on Maven repositories.
Full pom.xml Example for Java 25
Below is a complete pom.xml for a Java 25 console application: coordinates, description, properties with the Java version and encoding, a JUnit 5 test dependency, the output file name, resource filtering and pinned versions of the standard plugins. For Java 21, replace 25 with 21.
<?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>
<!-- project coordinates (GAV) -->
<groupId>com.examclouds</groupId>
<artifactId>shop</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- human-readable description -->
<name>Online Shop</name>
<description>A training online shop in Java</description>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.version>5.11.4</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>shop</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<pluginManagement>
<!-- plugin versions: check the latest version on Maven Central -->
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project> mvn package
# Output (fragment, the project has one test):
[INFO] Building Online Shop 1.0-SNAPSHOT
...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] Building jar: /home/user/shop/target/shop.jar
[INFO] BUILD SUCCESS If the project has no tests, you will see No tests to run. instead of the Tests run line. If the compiler used for the build (the JDK running Maven or a JDK selected via toolchains) does not support --release 25, the build stops at compilation with an error like release version 25 not supported; the exact text depends on the JDK and the maven-compiler-plugin version.
Common pom.xml Mistakes
Most pom.xml problems are not XML errors: the file is valid and the build often even succeeds, but the result is not what you expected. The table shows what you will see in the output or at runtime and how to fix it.
| What was written | What happens | How to fix it |
|---|---|---|
<sourceDirectory>src</sourceDirectory> with the standard layout | Java sources in the whole src tree, tests included, are compiled as main code; with JUnit Jupiter tests in test scope you get package org.junit.jupiter.api does not exist | Remove the line — the default src/main/java is already correct |
A plugin in <plugins> without <version>, when no version comes from a parent or pluginManagement (including the Super POM) | Warning 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-compiler-plugin is missing; the version depends on the installed Maven | Set the version in <plugin> or in pluginManagement; if a parent or pluginManagement already provides it, there is no need to repeat it locally |
Your own <resources> without src/main/resources | Files from src/main/resources are missing from the jar; getResourceAsStream returns null | List the standard directory in <resources> as well |
<filtering>true</filtering> on a directory with fonts and other binary files | Binary files may get corrupted while copying (filtering reads them as text). The plugin skips jpg, jpeg, gif, bmp and png by default, but fonts and other formats are still filtered | Filter a separate directory or only specific files via <includes>, and copy the rest with a second <resource> block without filtering — otherwise they are left out of the build; or add the extensions to nonFilteredFileExtensions (documentation) |
${pom.version} or ${version} | Warning The expression ${pom.version} is deprecated | Use ${project.version} |
<packaging>pom</packaging> in a project with code | BUILD SUCCESS, but no classes are compiled and no jar is created | Remove packaging (it becomes jar) — pom is for parent POMs, aggregators, BOMs and other projects that publish only a POM |
Expecting finalName to change the name in ~/.m2 | In the local repository the file is still named artifactId-version.jar | This is expected: dependencies are resolved by GAV, not by file name |
Frequently Asked Questions
Can the POM file have a name other than pom.xml?
Yes, but then you have to pass it on every run: mvn -f build-pom.xml package (the long form is --file). Without this flag Maven looks for pom.xml in the current directory, and IDEs and CI servers expect the standard name too, so do not rename the file without a good reason.
What is the difference between pom.xml and settings.xml?
pom.xml describes a specific project and lives in its Git repository. settings.xml holds Maven settings for a particular machine: the user file ~/.m2/settings.xml and the global conf/settings.xml in the Maven installation directory. They define the local repository path, mirrors, proxies, and logins and passwords for corporate repositories. That is why passwords never go into pom.xml. Repository types are covered in a separate lesson of this module.
How do I read the project version from pom.xml in Java code?
A convenient way is resource filtering: the line app.version=${project.version} in src/main/resources/app.properties becomes app.version=1.0-SNAPSHOT during the build, and your code reads the file with Properties and getResourceAsStream. The setup is shown in The build Section. It is not the only way and not always reliable in an IDE: if the build is not delegated to Maven, filtering may not run. By default maven-jar-plugin also puts META-INF/maven/{groupId}/{artifactId}/pom.properties with the version into the jar, but this file is absent when you run from the IDE (from target/classes).
Is artifactId inherited from the parent POM?
No. A child inherits groupId, version, properties, dependencies and plugin configuration from the parent, but every module must declare its own artifactId — otherwise two modules would have identical coordinates. groupId and version are usually omitted in a child module when they match the parent. More details in parent, Super POM and Effective POM.
Comments