1. Difference Between dependencyManagement
and dependencies
dependencyManagement
- Manages only versions of dependencies.
- Sub-modules must explicitly declare the dependency to use it.
- Ensures version consistency across all modules.
dependencies
- Adds the dependency directly to the project.
- Automatically includes dependencies in sub-modules.
- Does not enforce a single version across modules.
📌 When to Use Which?
Feature | dependencyManagement |
dependencies |
---|---|---|
Purpose | Manages only versions | Adds dependencies to the project |
Automatically Inherited? | ❌ No | ✅ Yes |
Controls Version Consistency? | ✅ Yes | ❌ No |
Best Use Case | Multi-module projects where dependencies are shared but not always needed | Dependencies required by all sub-modules |
2. Centralized Version Control for Multi-Module Projects
Why Centralize Dependency Versions?
✅ Ensures consistency across all sub-modules.
✅ Prevents version conflicts when multiple sub-modules use the same dependencies.
✅ Simplifies maintenance—update one place instead of multiple sub-modules.
✅ Avoids redundant versions in sub-modules.
How to Do It Correctly?
📌 Parent POM (Centralized Version Management)
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.4.0</version>
</dependency>
</dependencies>
</dependencyManagement>
📌 Sub-Module POM (No Version Needed)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
✅ Effect: The sub-module inherits the version from the parent without needing to define it.
3. Additional Best Practices
3️⃣ Example of Optional vs. Non-Optional Dependencies
❌ Without <optional>true>
(Bad Practice)
If spring-boot-devtools
is not marked as optional, other modules that depend on config-service
will inherit it, even if they don’t need it.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
✅ Effect:
- All dependent microservices (e.g.,
auth-service
,member-service
) will inheritspring-boot-devtools
unnecessarily. - Can cause unexpected runtime behavior in production.
✅ Correct Way: Marking It as Optional
By marking spring-boot-devtools
as optional, it ensures only config-service
can use it without affecting other modules.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional> <!-- ✅ Prevents transitive inheritance -->
</dependency>
✅ Effect:
config-service
can usedevtools
, but other modules won’t inherit it.- Production deployments remain clean and optimized.
🔹 Using BOM (Bill of Materials) for External Dependencies
Instead of manually managing versions, use a BOM (import
scope) to handle versions automatically.
📌 Example: Importing a BOM in Parent POM
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.4.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
✅ Effect: All Spring Boot dependencies automatically use compatible versions.
🔹 Using <properties>
for Version Variables
Instead of hardcoding versions, store them in <properties>
.
📌 Example
<properties>
<spring.boot.version>3.4.0</spring.boot.version>
<jjwt.version>0.12.3</jjwt.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
✅ Effect: Only update one place to change a version across all modules.
4. Disadvantages of Using compile
Scope for Runtime-Only Dependencies
Using compile
scope for dependencies that are only needed at runtime can introduce several issues:
- Increased Application Size 📦 – Unnecessary libraries get included.
- Unnecessary Classpath Pollution ⚠️ – Other modules may depend on unneeded libraries.
- Security Risks 🔒 – More dependencies increase the attack surface.
- Harder Upgrades & Conflicts 🔄 – Version mismatches become more likely.
- Longer Build Times ⏳ – More dependencies slow down compilation.
📌 Example: The Problem with compile
Scope and the Correct Usage of runtime
Scope
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<!-- No scope needed, used at compile-time -->
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
❌ Problem with compile
Scope:
- If
jjwt-impl
andjjwt-jackson
were set tocompile
, they would be included in the classpath unnecessarily. - This would increase build size, slow down compilation, and potentially create version conflicts in dependent modules.
✅ Why Use runtime
Instead?
jjwt-api
is needed at compile-time (defines the methods and interfaces).jjwt-impl
andjjwt-jackson
are only needed at runtime for JWT parsing and serialization.- Using
runtime
scope keeps the build size small and prevents unnecessary dependencies from affecting other modules.
5. Final Summary
✔ Use dependencyManagement
in the parent POM to centralize dependency versions.
✔ Use dependencies
in sub-modules to include only required dependencies without versions.
✔ Use BOM (import
scope) for external dependencies like Spring Boot BOM.
✔ Use <properties>
for version variables instead of hardcoding versions.
✔ Avoid redundant dependencies in sub-modules—declare only what’s needed.
✔ Use runtime
scope for libraries only needed when the application runs (e.g., jjwt-impl
).