On this page

Maven Dependencies

Goal: Add Spring Boot, PostgreSQL, Lombok, etc. without manually downloading JARs.

Our Student Report App is growing. Right now, it reads marks and writes a JSON file. Now we want it to:

  • show reports in a browser (a web app, using Spring Boot),
  • save students and marks in a PostgreSQL database,
  • and write less repetitive code (using Lombok).

That's three new libraries, and each one needs many more JARs behind the scenes. In this page, we'll add them all without downloading a single JAR by hand.

What is a dependency?

Quick recap from the first page: a dependency is a library your project needs in order to compile or run.

So far, our app has one: Jackson, for creating JSON. Now it needs more:

NeedDependency
Run as a web appSpring Boot
Talk to PostgreSQLPostgreSQL JDBC driver
Skip writing getters and settersLombok
Test the appSpring Boot's testing library

Without Maven, this would mean finding and downloading dozens of JARs. With Maven, we just list what we need in pom.xml.

Adding dependencies in pom.xml

Dependencies go inside the <dependencies> section of pom.xml. Each library gets its own <dependency> block.

Here's the updated section for our app:

xml
1<dependencies>
2 <!-- Web app support -->
3 <dependency>
4 <groupId>org.springframework.boot</groupId>
5 <artifactId>spring-boot-starter-web</artifactId>
6 <version>3.5.0</version>
7 </dependency>
8
9 <!-- PostgreSQL driver -->
10 <dependency>
11 <groupId>org.postgresql</groupId>
12 <artifactId>postgresql</artifactId>
13 <version>42.7.3</version>
14 <scope>runtime</scope>
15 </dependency>
16
17 <!-- Lombok: generates getters, setters, constructors -->
18 <dependency>
19 <groupId>org.projectlombok</groupId>
20 <artifactId>lombok</artifactId>
21 <version>1.18.34</version>
22 <scope>provided</scope>
23 </dependency>
24
25 <!-- Testing -->
26 <dependency>
27 <groupId>org.springframework.boot</groupId>
28 <artifactId>spring-boot-starter-test</artifactId>
29 <version>3.5.0</version>
30 <scope>test</scope>
31 </dependency>
32</dependencies>

That's it. The next time you build the project (or your IDE reloads it), Maven downloads everything needed.

You may notice two things:

  • Jackson and JUnit are gone. We'll see why in the Transitive dependencies section.
  • Some dependencies have a <scope>. We'll cover that in the Dependency scopes section.

But first: where do these libraries come from, and how did we know what to write?

Maven Central

Maven Central is the main public library store for Java. It holds hundreds of thousands of libraries, including Spring Boot, PostgreSQL's driver, Lombok, and Jackson.

When Maven needs a library, it downloads it from Maven Central by default. You don't have to configure anything.

To find a library and its exact XML:

  1. Go to central.sonatype.com.
  2. Search for the library, for example lombok.
  3. Open the result and pick a version.
  4. Copy the Maven XML snippet into your <dependencies> section.

Most library websites also show this snippet in their "Getting Started" guide.

The snippet you copy is just the library's coordinates.

Dependency coordinates

In the previous page, we gave our own project coordinates: groupId, artifactId, and version. Every library on Maven Central has them too, and that's how you tell Maven exactly which library you want.

xml
1<dependency>
2 <groupId>org.postgresql</groupId> <!-- who made it -->
3 <artifactId>postgresql</artifactId> <!-- which library -->
4 <version>42.7.3</version> <!-- which release -->
5</dependency>

Written as a GAV:

text
1org.postgresql:postgresql:42.7.3

All three must be correct. One typo, and Maven can't find the library and stops with an error.

Of the three, version needs the most care.

Dependency versions

Most libraries use a version format called semantic versioning:

text
1MAJOR . MINOR . PATCH
2 3 . 5 . 0
PartChanges whenIs it safe to upgrade?
PATCH (3.5.0 → 3.5.1)Bugs are fixedUsually yes
MINOR (3.5 → 3.6)New features are addedUsually yes
MAJOR (3 → 4)Old features are changed or removedYour code may break

A few simple rules:

Always use a fixed version. Write 42.7.3, not "latest." A fixed version means everyone on your team, and every future build, gets the exact same library.

Avoid -SNAPSHOT versions of other people's libraries. As we saw earlier, a SNAPSHOT can change at any time, so your build could change without you touching it.

Keep shared versions in one place. Notice that both Spring Boot dependencies use 3.5.0. Instead of repeating it, you can store it as a property:

xml
1<properties>
2 <spring-boot.version>3.5.0</spring-boot.version>
3</properties>
xml
1<version>${spring-boot.version}</version>

Now upgrading Spring Boot means changing one line.

(In real Spring Boot projects, there's an even easier way: the Spring Boot parent, which picks compatible versions for you. We'll cover that later.)

Now let's look more closely at the dependencies we wrote.

Direct dependencies

A direct dependency is a library you add yourself in pom.xml.

Our app has four direct dependencies:

text
1spring-boot-starter-web
2postgresql
3lombok
4spring-boot-starter-test

These are the libraries you chose, and the ones your own code uses. You control their versions directly.

But four direct dependencies don't mean four JARs. Not even close.

Transitive dependencies

A transitive dependency is a library that one of your dependencies needs. You didn't ask for it, but Maven downloads it anyway, because without it your dependency won't work.

We saw a small example in the first page: jackson-databind needs jackson-core and jackson-annotations.

Spring Boot is the same idea at a bigger scale. spring-boot-starter-web needs:

  • Spring's web framework, to handle web requests,
  • an embedded Tomcat server, to run the web app,
  • Jackson, to turn objects into JSON responses,
  • and many more.

That's why we removed Jackson from our pom.xml. Spring Boot already brings it in. The same goes for JUnit: spring-boot-starter-test includes it.

This is also why Spring Boot libraries are called starters. One starter brings in a whole set of libraries that work well together.

So how many JARs does our app actually have? Maven can show us.

Dependency tree

Maven can print every dependency your project uses, showing which library brought in which. Run:

text
1mvn dependency:tree

Here's a simplified part of the output for our app:

text
1com.example:student-report-app:jar:1.0.0-SNAPSHOT
2├── org.springframework.boot:spring-boot-starter-web:3.5.0
3│ ├── spring-boot-starter
4│ │ ├── spring-boot
5│ │ ├── spring-boot-autoconfigure
6│ │ └── spring-boot-starter-logging
7│ ├── spring-boot-starter-json
8│ │ └── jackson-databind
9│ │ ├── jackson-core
10│ │ └── jackson-annotations
11│ ├── spring-boot-starter-tomcat
12│ │ └── tomcat-embed-core
13│ ├── spring-web
14│ └── spring-webmvc
15├── org.postgresql:postgresql:42.7.3 (runtime)
16├── org.projectlombok:lombok:1.18.34 (provided)
17└── org.springframework.boot:spring-boot-starter-test:3.5.0 (test)
18 ├── junit-jupiter
19 ├── mockito-core
20 └── ...

How to read it:

  • Top level (first indent): your direct dependencies.
  • Deeper levels: transitive dependencies, and who brought them in.

The real output is much longer. Our four direct dependencies pull in dozens of JARs. Imagine managing that by hand.

The tree is also your best tool when something goes wrong. If you wonder "where did this library come from?" or "why is this version being used?", the tree answers it.

For example, if two libraries need different versions of the same JAR, Maven picks the one closest to your project in the tree. A version you write directly in pom.xml always wins over a transitive one. That's another reason we removed our own Jackson: keeping it would have overridden the version Spring Boot was tested with.

You may have noticed the labels (runtime), (provided), and (test) in the tree. Those are scopes.

Dependency scopes: compile, test, provided, runtime, system

Not every library is needed all the time. A scope tells Maven when a dependency is needed: while compiling, while testing, while running, and whether it goes into the final app.

Our app uses four scopes, one for each kind of library.

compile: needed everywhere (the default). If you don't write a scope, it's compile. The library is available when compiling, testing, and running, and is shipped with your app.

Example: spring-boot-starter-web. Our code uses Spring classes directly, and the app needs them while running.

test: needed only for tests. Available only for code in src/test. Not part of the final app.

Example: spring-boot-starter-test. Users of our app don't need our testing tools.

provided: needed to compile, but not shipped. Available while compiling and testing, but not packaged. Use it when something else will provide the library later, or when it's only needed at compile time.

Example: lombok. Lombok writes getters and setters while compiling. After that, the generated code is already inside our .class files, so Lombok itself isn't needed when the app runs.

runtime: not needed to compile, but needed to run. Your code doesn't use it directly, but the app needs it while running.

Example: postgresql. Our code talks to the database through Java's standard database classes (java.sql), never through PostgreSQL's own classes. The driver is only needed when the app actually connects to the database.

system: a JAR from a path on your computer. Like provided, but you point Maven to a JAR file on your own machine instead of downloading it. Avoid this scope. It is deprecated, and it brings back the old problem: the build only works on machines that have that exact file in that exact place.

Here's everything in one table:

ScopeCompileTestRunIn final appOur example
compile✓✓✓✓spring-boot-starter-web
test✓spring-boot-starter-test
provided✓✓lombok
runtime✓✓✓postgresql
system✓✓Avoid

A simple way to choose: start with no scope (compile). Use test for testing libraries, runtime for drivers, and provided for compile-time tools like Lombok.

Now we know what we're asking Maven for. Let's look at what Maven actually does with that request.

What happens when Maven downloads a dependency?

When you build the project, Maven goes through these steps for each dependency:

  1. Read pom.xml. Maven sees, for example, org.postgresql:postgresql:42.7.3.
  2. Check your computer first. Maven looks in its local storage folder. If the library is already there, it skips the download.
  3. Download from Maven Central. If it's missing, Maven downloads two files:
    • the library's .jar, which contains its code,
    • the library's own .pom, which lists its dependencies.
  4. Check the file. Maven checks the download against a checksum, to make sure the file isn't broken.
  5. Repeat for transitive dependencies. Maven reads the library's .pom, finds what it needs, and repeats these steps for each of those. This is how one starter becomes dozens of JARs.
  6. Pick one version of each library. If the same library appears more than once with different versions, Maven picks one, as we saw in the dependency tree.
  7. Build the classpath. Maven adds every needed JAR to the classpath, based on scope.

Step 7 is the long -cp command from the first page, done automatically.

The first build of a new project can take a while because of all the downloads. Later builds are fast, because of step 2: the libraries are already stored on your computer.

So where exactly are they stored?

Local Maven repository (~/.m2/repository)

Every library Maven downloads is saved in one folder on your computer, called the local repository:

OSLocation
macOS / Linux~/.m2/repository
WindowsC:\Users\<your-name>\.m2\repository

As we saw in the previous page, the folder path comes from the GAV. For the PostgreSQL driver:

text
1~/.m2/repository/org/postgresql/postgresql/42.7.3/
2├── postgresql-42.7.3.jar
3├── postgresql-42.7.3.pom
4└── postgresql-42.7.3.jar.sha1

These are the files from the download steps: the code (.jar), its dependency list (.pom), and its checksum (.sha1).

Why this folder matters:

It is shared by all your projects. If ten projects use Spring Boot 3.5.0, it is downloaded once and reused by all ten. No more lib folders in every project.

It makes builds fast. Once a library is here, Maven doesn't download it again.

It works offline. If all the libraries are already downloaded, you can build without internet.

It is safe to delete. If a download gets broken or something behaves strangely, you can delete the library's folder (or the whole repository folder). Maven will download it again on the next build.

Don't commit it or share it. Just like target/, it's Maven's working space. Your team shares pom.xml, and Maven on each machine fills its own local repository.

And that completes the picture. Compare where we started with where we are now:

Without MavenWith Maven
Search the web for each JARCopy coordinates from Maven Central
Find each library's own JARs yourselfTransitive dependencies downloaded automatically
Guess which versions work togetherFixed versions in pom.xml, one version picked per library
A lib folder in every projectOne shared local repository
Write a long classpath by handClasspath built automatically, based on scope

We added Spring Boot, PostgreSQL, and Lombok, along with dozens of JARs behind them, by writing a few lines of XML.

Next TopicMaven Build Lifecycle & Commands