Back to Articles
Maven

Understanding POM.xml: What Every Java Developer Uses but Does Not Always Understand

Jul 2026 · · BORGES.

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:

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:

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:

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:

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:

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:

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:

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:

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:

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?

dependencies dependency security dependencyManagement Java Java Development Java Maven Java Programming POM.xml
Get in touch.

send me an email.

✉ contact@borges.digital GitHub LinkedIn