Definition
- Transitive dependency resolution -- declare one library and Maven downloads it plus everything that library depends on, automatically.
- Standard directory structure -- every Maven project uses src/main/java, src/test/java, src/main/resources, and target, so any engineer knows where to find everything.
- Fixed lifecycle -- ordered phases (validate through deploy) that run the same way on every machine, eliminating works-on-my-machine surprises.
- BOM (Bill of Materials) -- a special POM that only lists versions of a coordinated set of libraries, ensuring every service uses the same tested combination.
- Dependency scopes -- compile, provided, runtime, and test control where each dependency is visible.
- Plugin model -- lets you bind extra work like SonarQube analysis or Docker builds to lifecycle phases.
Think of Maven as the standardized operations manual for every branch of National Bank of India. Dependency management is the central supply chain -- you request the standard teller kit and Maven delivers it plus everything that kit depends on. The standard structure is every branch having the counter in the same place, so any employee can walk in and work. The lifecycle is the fixed opening routine every branch follows. The BOM is the head-office supply list that guarantees every branch orders the exact same compatible versions.
Understanding the Concepts
What is transitive dependency management?
Before tools like Maven, you downloaded jar files by hand and had to find every library those libraries needed. Maven solves this with declarative transitive dependency resolution. You declare one dependency, and Maven reads that library's POM, discovers everything it depends on, and downloads the whole graph automatically from Maven Central into your local .m2 repository.
<!-- pom.xml for Loan Approval Service --syn-kw">>
syn-kw"><projectsyn-kw">>
syn-kw"><groupIdsyn-kw">>com.bank.loanssyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>loan-approval-servicesyn-kw"></artifactIdsyn-kw">>
syn-kw"><versionsyn-kw">>1.0.0syn-kw"></versionsyn-kw">>
syn-kw"><parentsyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-starter-parentsyn-kw"></artifactIdsyn-kw">>
syn-kw"><versionsyn-kw">>3.2.0syn-kw"></versionsyn-kw">>
syn-kw"></parentsyn-kw">>
syn-kw"><dependenciessyn-kw">>
<!-- Declare ONE starter, Maven resolves 30+ transitive jars --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-starter-websyn-kw"></artifactIdsyn-kw">>
syn-kw"></dependencysyn-kw">>
syn-kw"></dependenciessyn-kw">>
syn-kw"></projectsyn-kw">>
mvn dependency:tree
com.bank.loans:loan-approval-service:jar:1.0.0
+- org.springframework.boot:spring-boot-starter-web:jar:3.2.0
| +- org.springframework.boot:spring-boot-starter:jar:3.2.0
| | +- org.springframework.boot:spring-boot:jar:3.2.0
| | +- org.springframework.boot:spring-boot-autoconfigure:jar:3.2.0
| +- org.springframework:spring-web:jar:6.1.2
| +- org.springframework:spring-webmvc:jar:6.1.2
| +- com.fasterxml.jackson.core:jackson-databind:jar:2.15.3
| +- org.apache.tomcat.embed:tomcat-embed-core:jar:10.1.17
What is the standard directory structure?
Every Maven project puts main source code under src/main/java, resources under src/main/resources, test code under src/test/java, and build output under target. This convention over configuration means a new engineer opening any project knows exactly where everything is.
loan-approval-service/
pom.xml
src/
main/
java/
com/bank/loans/
LoanApprovalApplication.java
service/
LoanApprovalService.java
model/
LoanApplication.java
repository/
LoanApplicationRepository.java
resources/
application.yml
db/migration/
V1__create_loan_tables.sql
test/
java/
com/bank/loans/
service/
LoanApprovalServiceTest.java
target/
loan-approval-service-1.0.0.jar
How does the fixed lifecycle make builds repeatable?
Maven defines ordered phases, and running a later phase automatically runs all earlier ones. So mvn package compiles, tests, and packages in that order, every time, on every machine. This eliminates works-on-my-machine surprises.
# On developer laptop
mvn clean package
# On CI server -- exact same command, exact same behavior
mvn clean package
[INFO] --- maven-compiler-plugin:3.11.0:compile ---
[INFO] Compiling 12 source files to target/classes
[INFO] --- maven-surefire-plugin:3.2.2:test ---
[INFO] Tests run: 18, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- maven-jar-plugin:3.3.0:jar ---
[INFO] Building jar: target/loan-approval-service-1.0.0.jar
[INFO] BUILD SUCCESS
package com.bank.loans.service;
import com.bank.loans.model.LoanApplication;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
@Service
public class LoanApprovalService {
private static final BigDecimal MAX_DTI_RATIO = new BigDecimal("0.40");
public boolean isEligible(LoanApplication application) {
BigDecimal dtiRatio = application.getMonthlyEmi()
.divide(application.getMonthlyIncome(), 4, RoundingMode.HALF_UP);
return dtiRatio.compareTo(MAX_DTI_RATIO) <= 0;
}
}
Loan application for Rajesh:
Monthly income: 80000.00
Monthly EMI: 25000.00
DTI ratio: 0.3125
Eligible: true
What is a BOM and why does it matter?
A BOM (Bill of Materials) is a special POM that only lists versions of a coordinated, tested set of libraries in its dependencyManagement section. When you import a BOM, you declare dependencies without versions, and Maven fills in the tested versions. This prevents version conflicts where two libraries pull in different, incompatible versions of the same transitive dependency.
<!-- Import Spring Boot BOM for version alignment --syn-kw">>
syn-kw"><dependencyManagementsyn-kw">>
syn-kw"><dependenciessyn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-dependenciessyn-kw"></artifactIdsyn-kw">>
syn-kw"><versionsyn-kw">>3.2.0syn-kw"></versionsyn-kw">>
syn-kw"><typesyn-kw">>pomsyn-kw"></typesyn-kw">>
syn-kw"><scopesyn-kw">>importsyn-kw"></scopesyn-kw">>
syn-kw"></dependencysyn-kw">>
syn-kw"></dependenciessyn-kw">>
syn-kw"></dependencyManagementsyn-kw">>
syn-kw"><dependenciessyn-kw">>
<!-- No version needed -- BOM provides it --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-starter-websyn-kw"></artifactIdsyn-kw">>
syn-kw"></dependencysyn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>com.fasterxml.jackson.coresyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>jackson-databindsyn-kw"></artifactIdsyn-kw">>
<!-- BOM ensures this matches the Spring Boot tested version --syn-kw">>
syn-kw"></dependencysyn-kw">>
syn-kw"></dependenciessyn-kw">>
How do dependency scopes work?
Scopes control where each dependency is visible. Compile scope is the default and is available everywhere. Provided means the dependency is needed to compile but is supplied at runtime by the container. Runtime means it is not needed to compile but is needed at runtime. Test means it is only available during test compilation and execution.
syn-kw"><dependenciessyn-kw">>
<!-- compile: available everywhere --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-starter-websyn-kw"></artifactIdsyn-kw">>
syn-kw"></dependencysyn-kw">>
<!-- provided: Lombok needed to compile, not in final jar --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.projectlomboksyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>lomboksyn-kw"></artifactIdsyn-kw">>
syn-kw"><scopesyn-kw">>providedsyn-kw"></scopesyn-kw">>
syn-kw"></dependencysyn-kw">>
<!-- runtime: JDBC driver not needed to compile --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.postgresqlsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>postgresqlsyn-kw"></artifactIdsyn-kw">>
syn-kw"><scopesyn-kw">>runtimesyn-kw"></scopesyn-kw">>
syn-kw"></dependencysyn-kw">>
<!-- test: JUnit only during testing --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.springframework.bootsyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>spring-boot-starter-testsyn-kw"></artifactIdsyn-kw">>
syn-kw"><scopesyn-kw">>testsyn-kw"></scopesyn-kw">>
syn-kw"></dependencysyn-kw">>
syn-kw"></dependenciessyn-kw">>
How does the plugin model extend the lifecycle?
Plugins bind extra work to lifecycle phases. You can run static analysis with SonarQube, enforce coverage thresholds, build Docker images, or generate API documentation, all as part of the standard build command.
syn-kw"><buildsyn-kw">>
syn-kw"><pluginssyn-kw">>
<!-- JaCoCo for code coverage --syn-kw">>
syn-kw"><pluginsyn-kw">>
syn-kw"><groupIdsyn-kw">>org.jacocosyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>jacoco-maven-pluginsyn-kw"></artifactIdsyn-kw">>
syn-kw"><versionsyn-kw">>0.8.11syn-kw"></versionsyn-kw">>
syn-kw"><executionssyn-kw">>
syn-kw"><executionsyn-kw">>
syn-kw"><goalssyn-kw">>syn-kw"><goalsyn-kw">>prepare-agentsyn-kw"></goalsyn-kw">>syn-kw"></goalssyn-kw">>
syn-kw"></executionsyn-kw">>
syn-kw"><executionsyn-kw">>
syn-kw"><idsyn-kw">>checksyn-kw"></idsyn-kw">>
syn-kw"><phasesyn-kw">>verifysyn-kw"></phasesyn-kw">>
syn-kw"><goalssyn-kw">>syn-kw"><goalsyn-kw">>checksyn-kw"></goalsyn-kw">>syn-kw"></goalssyn-kw">>
syn-kw"><configurationsyn-kw">>
syn-kw"><rulessyn-kw">>
syn-kw"><rulesyn-kw">>
syn-kw"><limitssyn-kw">>
syn-kw"><limitsyn-kw">>
syn-kw"><countersyn-kw">>LINEsyn-kw"></countersyn-kw">>
syn-kw"><valuesyn-kw">>COVEREDRATIOsyn-kw"></valuesyn-kw">>
syn-kw"><minimumsyn-kw">>0.80syn-kw"></minimumsyn-kw">>
syn-kw"></limitsyn-kw">>
syn-kw"></limitssyn-kw">>
syn-kw"></rulesyn-kw">>
syn-kw"></rulessyn-kw">>
syn-kw"></configurationsyn-kw">>
syn-kw"></executionsyn-kw">>
syn-kw"></executionssyn-kw">>
syn-kw"></pluginsyn-kw">>
syn-kw"></pluginssyn-kw">>
syn-kw"></buildsyn-kw">>
mvn clean verify
[INFO] --- jacoco-maven-plugin:0.8.11:check ---
[INFO] Loading execution data file target/jacoco.exec
[INFO] Analyzed bundle 'loan-approval-service' with 12 classes
[INFO] All coverage checks passed: line coverage 87% >= 80%
[INFO] BUILD SUCCESS
What are common mistakes?
The most common mistakes are not using a BOM leading to version conflicts, specifying test dependencies without the test scope so they end up in production, and manually managing transitive jars instead of letting Maven resolve them.
<!-- WRONG -- test library leaks into production jar --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.junit.jupitersyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>junit-jupitersyn-kw"></artifactIdsyn-kw">>
syn-kw"><versionsyn-kw">>5.10.1syn-kw"></versionsyn-kw">>
<!-- missing scope:test --syn-kw">>
syn-kw"></dependencysyn-kw">>
<!-- CORRECT -- test scope keeps JUnit out of production --syn-kw">>
syn-kw"><dependencysyn-kw">>
syn-kw"><groupIdsyn-kw">>org.junit.jupitersyn-kw"></groupIdsyn-kw">>
syn-kw"><artifactIdsyn-kw">>junit-jupitersyn-kw"></artifactIdsyn-kw">>
syn-kw"><versionsyn-kw">>5.10.1syn-kw"></versionsyn-kw">>
syn-kw"><scopesyn-kw">>testsyn-kw"></scopesyn-kw">>
syn-kw"></dependencysyn-kw">>
What should a senior engineer know?
A senior engineer knows that the discipline Maven imposes -- predictability, auditability, uniformity -- is exactly why regulated industries like banking favor it. They know that the parent POM centralizes plugin versions and configuration across dozens of microservices, that the enforcer plugin can fail the build if banned dependencies appear, and that consistent, tested versions across all services are a correctness and security requirement.
Visual Overview
Maven Dependency Resolution
───────────────────────────
┌─────────────┐
│ pom.xml │ you declare: spring-boot-starter-web
└──────┬──────┘
│
▼
┌─────────────────────────────┐
│ Maven Central Repository │
│ Download starter-web POM │
│ Read its dependencies │
│ Download their POMs │
│ Repeat recursively │
└──────────┬──────────────────┘
│
▼
┌─────────────────────────────┐
│ Local .m2 Repository │
│ ~/.m2/repository/ │
│ ├── spring-boot-3.2.0.jar │
│ ├── spring-web-6.1.2.jar │
│ ├── jackson-2.15.3.jar │
│ └── tomcat-embed-10.1.17 │
└─────────────────────────────┘
BOM Version Alignment:
┌──────────────────────────┐
│ spring-boot-dependencies │
│ (BOM) │
│ jackson: 2.15.3 │
│ spring: 6.1.2 │
│ tomcat: 10.1.17 │
└───────────┬──────────────┘
│ all versions coordinated
▼
┌──────────────────────────┐
│ Your pom.xml │
│ No version needed -- │
│ BOM fills them in │
└──────────────────────────┘
Key Takeaway
We use Maven because it makes Java builds predictable, reproducible, and uniform. Transitive dependency resolution means you declare one library and Maven downloads the entire graph, so you never chase jars by hand. The standard directory structure means any engineer opening any project knows exactly where to find everything. The fixed lifecycle runs the same ordered phases on every machine, eliminating works-on-my-machine surprises.
The reason to emphasize most is BOM-based version management. A Bill of Materials is a POM that lists a coordinated, tested set of library versions. When you import a BOM, you declare dependencies without versions and Maven fills them in, so every service uses the exact same tested combination. This prevents the subtle, dangerous bug where two libraries drag in different versions of the same transitive dependency.
Scopes, plugins, and the parent POM complete the picture. Scopes keep test libraries out of production. Plugins bind extra work like coverage gates and SonarQube analysis to lifecycle phases so the build is a single command. The parent POM centralizes configuration across dozens of microservices so standards are enforced organization-wide.
Remember one sentence: Maven gives transitive dependency resolution, a universal project layout, a repeatable lifecycle, and BOM-driven version alignment, which is why regulated industries like banking standardize on it.
---