If you work with Java, especially with Spring Boot, you have probably opened a file called pom.xml. Maybe you have already copied a dependency from Maven Repository, pasted it into that file, ran the application, and moved on.
But pom.xml is much more than a place to paste dependencies. It is one of the central pieces of a Java project that uses Maven. It is where the project says who it is, what it depends on, how it is built, how it will be packaged, and which tools participate in the build.
Understanding this file means moving away from the “copy and paste XML” mode and starting to see the real process of building a Java application.
pom.xml is not just configuration. It is the build contract of your Java project.
Why do we need Maven?
Before talking about pom.xml, it is worth understanding why Maven exists.
In a small project, it may seem simple to download a .jar file, manually place it on the classpath, and compile. But that does not hold up in real applications.
A modern Java application may depend on dozens of libraries: Spring Boot, database drivers, validation, logging, testing, messaging, security, JSON, API documentation, observability, and much more.
Now imagine controlling all of this manually:
- downloading each library in the correct version;
- figuring out which other libraries it needs;
- resolving conflicts between versions;
- compiling the project the same way every time;
- running tests;
- packaging the application;
- making sure another developer can run the same project.
That is exactly where Maven comes in.
It standardizes how the project is built, manages dependencies, executes plugins, follows build phases, and allows the project to be built predictably in any environment.
Maven does not exist only to download dependencies. It exists to make the project build reproducible.
Is Maven still that widely used?
Yes. Even with alternatives like Gradle, Maven remains very strong in the Java ecosystem. According to JetBrains’ State of Java 2025 report, Maven appears as the most used build system among Java developers, with 67% usage.
This does not mean Maven is always better than Gradle. Each tool has its place. Gradle appears a lot in Android projects and in more customized builds. Maven, on the other hand, remains very present in enterprise Java applications, Spring Boot projects, libraries, APIs, and both legacy and modern systems.
One of the reasons is predictability. Maven follows strong conventions. The project structure, build phases, and POM format create a standard that makes life easier for larger teams.
In other words: learning Maven is still very relevant for anyone who works or wants to work professionally with Java.
What is POM?
POM stands for Project Object Model. Maven’s official documentation defines the POM as the fundamental unit of work in Maven: an XML file with project information and configuration details used to build the application.
When you run a command like:
mvn clean package
Maven looks for the pom.xml, reads that file, and understands what it needs to do.
The POM tells Maven things like:
- what the project is;
- which version is being built;
- which dependencies must be downloaded;
- which plugins participate in the build;
- which Java version will be used;
- how the project will be packaged;
- which configurations are inherited from other POMs.
That is why, when someone says “it is just XML”, they probably still have not understood the role of this file.
The project identity: groupId, artifactId, and version
A minimal POM usually has this structure:
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>digital.borges</groupId>
<artifactId>my-api</artifactId>
<version>1.0.0</version>
</project>
These three fields are the identity of the artifact:
- groupId: usually represents the organization, domain, or project group;
- artifactId: the name of the artifact that will be generated;
- version: indicates the version of that artifact.
Together, they form something like:
digital.borges:my-api:1.0.0
This address identifies your application or library inside the Maven ecosystem.
It is the same principle used when you add a dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
You are telling Maven: “go fetch this artifact for my project”.
The POM you write is not the complete POM
Here begins a part that many people use, but few truly understand: the POM you write is not necessarily the complete POM Maven uses.
Maven works with inheritance, defaults, parent POMs, the Super POM, and inherited configurations. The official documentation explains that the POM has default values for many projects, such as source code, test, and build directories.
This means that, even if you do not write certain configurations, they may exist in the effective POM of the project.
To see the complete POM that Maven is actually considering, run:
mvn help:effective-pom
This command shows the effective POM, which is the final POM after applying inheritance, defaults, and configurations from the parent.
This is very useful when you ask questions like:
- where did this dependency version come from?
- why is this plugin being executed?
- which configuration is being inherited?
- why did I not need to declare the version of this starter?
Many answers are in the effective POM, not only in the file you opened in the IDE.
Parent POM: inheritance that simplifies and also hides
In Spring Boot projects, it is common to find something like this:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath/>
</parent>
This block says that your project inherits configurations from the Spring Boot parent POM.
In practice, this makes many things easier. You can declare dependencies without specifying a version because the parent already manages several versions for you.
For example:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Notice that there is no <version>. This does not mean the dependency has no version. It means the version is being controlled somewhere else.
This is great for maintaining consistency, but it can also be confusing. If you do not know there is version management behind the scenes, you may get lost when you need to update, override, or resolve a conflict.
Dependency Management does not add a dependency
Another point many people confuse is dependencyManagement.
It does not automatically add a dependency to the project. It only defines how a dependency should be managed if it is used.
Example:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>
</dependencyManagement>
This block says: “if someone uses jackson-databind, use this version”.
But for the dependency to actually enter the project, it must appear in dependencies or come transitively through another library.
This difference matters. dependencyManagement controls versions. dependencies declares what the project uses.
Transitive dependency: what enters your project without you noticing
When you add a dependency in Maven, it often brings other dependencies along with it. This is called a transitive dependency. Maven’s official documentation explains that this feature prevents you from having to discover and manually declare all the libraries required by your direct dependencies.
This is extremely useful, but it is also one of the most important parts to understand.
When you add:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
you are not bringing just one file. You are bringing a set of libraries required for that starter to work.
To see this, run:
mvn dependency:tree
This command shows the project dependency tree.
A simplified example could be:
[INFO] digital.borges:my-api:jar:1.0.0
[INFO] \- org.springframework.boot:spring-boot-starter-web:jar:3.4.0
[INFO] +- org.springframework.boot:spring-boot-starter-json:jar:3.4.0
[INFO] +- org.springframework.boot:spring-boot-starter-tomcat:jar:3.4.0
[INFO] \- org.springframework:spring-webmvc:jar:6.2.0
This tree tells a story. It shows what you requested directly and what entered the project because another library needed it.
Many build, conflict, and security problems start exactly there: in a dependency you did not even know you were using.
Scope: where does the dependency enter?
Another detail many people ignore is scope.
The scope defines when that dependency will be used: compilation, tests, runtime, or packaging.
Common example:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Here, this dependency should be used only in tests.
This prevents testing libraries from being taken to places where they do not make sense.
Another example:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
Lombok is used during development and compilation, but you do not want to treat it as a regular dependency of the application’s business logic.
Understanding scope helps avoid a polluted POM, unnecessary packaging, and dependencies appearing where they should not.
Plugin is not dependency
This confusion is very common.
A dependency is something your application uses. A plugin is something Maven uses during the build.
Plugin example:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
This plugin helps with the packaging and execution process of a Spring Boot application. It is not a library that you inject into a service or repository.
Keep this difference in mind:
- dependencies: libraries used by the application;
- plugins: tools used by Maven during the build process.
When you understand this, the POM starts to feel less mysterious.
Packaging: what will Maven generate?
The packaging field defines the type of artifact generated by the project.
<packaging>jar</packaging>
The most common values are:
- jar: common in Java and Spring Boot applications;
- war: used in web applications packaged for external servers;
- pom: used in parent or multi-module projects.
If you do not declare packaging, Maven assumes jar by default.
This detail may seem small, but it directly affects what will be generated when you run:
mvn package
When a dependency brings a vulnerability
Here is a part every developer should take more seriously: dependency also means attack surface.
Every library added to the project is third-party code entering your application. And when that library brings other transitive libraries, the surface grows.
Sometimes, the vulnerability is not in the dependency you added directly, but in an indirect dependency that came along with it.
Some modern IDEs, such as IntelliJ IDEA, can already warn about vulnerable dependencies directly in the project. Repositories on GitHub can also use Dependabot, which identifies vulnerable or outdated dependencies and can automatically suggest updates.
In addition, there are specific tools for software composition analysis, known as SCA tools, such as OWASP Dependency-Check, Snyk, Sonatype, Mend, and others. They analyze the libraries used by the project and cross-check them with known vulnerability databases.
This changes how we should look at the POM.
Adding a dependency should not be an automatic gesture. Before pasting another XML block, it is worth thinking:
- is this library really necessary?
- is it still maintained?
- what was the latest version released?
- does it bring many transitive dependencies?
- are there known vulnerability alerts?
- is there something in the framework itself that already solves this problem?
How to investigate a problematic dependency
The first step is to look at the tree:
mvn dependency:tree
If you want to filter by a specific library:
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
This helps answer a very common question:
“Where did this library come from?”
Once you discover the origin, there are a few possible paths.
1. Update the direct dependency
If the vulnerable library was declared directly in your POM, the first path is usually to update it to a fixed version:
<dependency>
<groupId>com.example</groupId>
<artifactId>example-library</artifactId>
<version>2.1.4</version>
</dependency>
But this needs to be followed by testing. Updating a dependency may fix a vulnerability and, at the same time, break behavior if there was an API change.
2. Control the version with dependencyManagement
If the dependency comes transitively, you can control the version used in the project with dependencyManagement. Maven allows this mechanism to specify versions for artifacts used in transitive dependencies or in dependencies without an explicit version.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>transitive-lib</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
</dependencyManagement>
This feature is very useful, but it needs to be used responsibly. You are telling Maven to choose a specific version. That can fix a problem, but it can also create incompatibility if the main library expected another version.
3. Exclude a transitive dependency
In some cases, you can exclude a dependency that came along but should not be used:
<dependency>
<groupId>com.example</groupId>
<artifactId>main-lib</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.example</groupId>
<artifactId>vulnerable-lib</artifactId>
</exclusion>
</exclusions>
</dependency>
This must be done carefully. If the main library really needs that dependency, the exclusion may break the application at runtime.
4. Automate vulnerability analysis
A more professional way to handle this is to include dependency analysis in the build.
The OWASP Dependency-Check Maven plugin can be used in the build and requires Maven 3.6.3 or higher, according to the tool’s official documentation.
<build>
<plugins>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>12.1.0</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Then you can run:
mvn verify
The idea here is simple: dependency security should not depend only on someone remembering to check manually. It should be part of the normal build process.
The most common mistake: treating the POM as an XML dump
The biggest mistake is treating pom.xml as a file where you just paste things.
You find a library, copy the XML, paste it, it works, and that is it.
The problem is that, over time, the POM can become an accumulation of decisions nobody understands:
- dependencies that are no longer used;
- versions declared without need;
- forgotten plugins;
- exclusions added without explanation;
- wrong scopes;
- vulnerable transitive dependencies;
- inherited configurations nobody knows where they came from.
A messy POM is a warning sign. It may indicate that the project build also lacks clarity.
What I look at in an unknown POM
When I open a Java project I do not know, one of the first things I look at is the pom.xml.
It quickly shows me:
- which Java version the project uses;
- which main framework is there;
- whether it is Spring Boot and which parent is being used;
- which starters enter the application;
- whether there are duplicate or strange dependencies;
- how tests are configured;
- how the project is packaged;
- whether there are build, security, coverage, or quality plugins;
- whether the project looks organized or has become an accumulation of patches.
The POM tells the technical story of the project.
Sometimes, even before running the application, it already shows maturity, technical debt, carelessness, or organization.
Conclusion
pom.xml is one of those files every Java developer uses, but not every Java developer understands.
It does not only declare dependencies. It defines identity, inheritance, build, plugins, packaging, versions, scopes, and an important part of the project’s security.
Understanding the POM means understanding how your application is born outside the IDE.
It means knowing where your dependencies come from, why a version was chosen, what is being inherited, which plugins participate in the build, and what risks you assume when adding third-party code.
In the end, mastering pom.xml is not about liking XML. It is about having control over the project you are building.
Before pasting the next dependency, stop for a few seconds and ask yourself: do I know what this is bringing into my application?