Choosing the Right Maven Dependency Scope for Servlet and Library Projects
Use Maven dependency scopes to keep compile, test, and runtime classpaths separate; mark container-supplied APIs as provided so they are not packaged.
25 Aug 2026, 18:23 UTC

Problem: a servlet class is missing at runtime or bundled where it does not belong
You add jakarta.servlet-api to a Maven project so the code compiles. At runtime the application server already supplies the servlet classes. If Maven packages the API jar in your application, the container may see two servlet APIs and fail with linkage errors. If you omit the dependency, compilation fails. The useful takeaway is to declare the servlet API with provided scope: Maven uses it for compilation and testing but does not include it in the packaged runtime artifact.
How Maven dependency scopes work
A dependency scope controls when a jar is on the classpath and whether it is transitive to other projects. Maven scopes are part of the Project Object Model (POM), the configuration in pom.xml. Transitive dependencies are libraries required by your declared dependencies; Maven resolves them automatically.
The most common scopes are:
compile: default. Available for compilation, testing, and runtime; passed to dependent projects.provided: available for compilation and testing, but expected to be supplied by the runtime container; not packaged.runtime: not needed for compilation, but required at runtime; not passed to the compile classpath of dependent projects.test: available only during testing; not packaged or transitive.
Worked configuration in pom.xml
This example declares a library, a servlet API supplied by the container, and a test framework. Replace the property values with the versions your organization approves.
4.0.0
com.example
web-app
1.0.0
war
33.2.1-jre
6.0.0
4.13.2
com.google.guava
guava
${guava.version}
compile
jakarta.servlet
jakarta.servlet-api
${servlet.api.version}
provided
junit
junit
${junit.version}
test
compile is the default, so the explicit scope is optional for Guava. It documents intent. The servlet dependency must stay provided because the servlet container supplies the classes. JUnit is test so it does not reach production.
Compare scopes in a table
| Scope | Compile classpath | Test classpath | Runtime/package | Transitive to dependents |
|---|---|---|---|---|
| compile | yes | yes | yes | yes |
| provided | yes | yes | no | no |
| runtime | no | yes | yes | no |
| test | no | yes | no | no |
Diagnose the resolved graph
Run the following in the project root directory, where pom.xml lives. It requires read access to project files and the ability to execute Maven; no elevated permissions are needed.
mvn dependency:tree -Dverbose=true > dependency-tree.txt
Inspect dependency-tree.txt. Look for the scope column or scope labels. Expected result: Guava appears with compile scope; Jakarta Servlet API appears with provided scope; JUnit appears with test scope. If Jakarta Servlet API appears without a scope or as compile, Maven may package it, which is the mistake this configuration avoids. Use -Dverbose=true to see omitted or conflicting transitive versions.
For a direct classpath check, run:
mvn dependency:build-classpath -Dmdep.outputFile=cp.txt
Open cp.txt. A provided dependency should not appear in the runtime classpath produced by a package-oriented build, though behavior depends on the plugin and packaging type. Do not treat this file as proof of the final server classpath; the container still supplies provided classes.
Limitations and common mistakes
- Do not mark a library your application needs at runtime as
provided. The result is typicallyNoClassDefFoundErrororClassNotFoundExceptionafter deployment. - Do not use
runtimefor a library needed during compilation; compilation will fail. provideddependencies are not transitive. If a library you depend on declares its own dependency as provided, your project must declare that dependency explicitly if your code uses it directly.- Versions in
<dependencyManagement>manage versions but do not add dependencies. You still declare each artifact in<dependencies>. - Avoid guessing scopes by copying examples. A standalone jar usually needs compile dependencies packaged or shaded; a web application usually needs provided servlet APIs.
Practical verification
Run mvn clean compile to confirm provided and compile dependencies resolve. Run mvn clean test to confirm test dependencies resolve. If the build succeeds and dependency-tree.txt shows the servlet API as provided, the configuration is consistent. If deployment still fails, check the server classpath for a second servlet API before changing the scope to compile. If the change regresses, restore the previous pom.xml from version control.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.