Maven Dependency Resolution
Learn Java Source, Package, Dependency, Build, Release & Deployment Engineering - Part 008
Deep dive Maven dependency resolution: direct dan transitive dependencies, scopes, optional, exclusions, dependency mediation, dependencyManagement, BOM, classpath, convergence, dan diagnostic workflow.
Part 008 — Maven Dependency Resolution
Dependency resolution adalah salah satu tempat paling penting untuk membedakan engineer yang “bisa pakai Maven” dari engineer yang mampu menjaga sistem besar tetap stabil. Banyak production bug Java bukan karena business logic salah, tetapi karena classpath berisi versi library yang tidak kita pikir sedang dipakai.
Di Part 007, kita mempelajari Maven sebagai project model, lifecycle executor, dan plugin runtime. Sekarang kita masuk ke mesin dependency resolution.
Pertanyaan inti bagian ini:
Ketika POM hanya mendeklarasikan beberapa dependency, bagaimana Maven menentukan ratusan JAR yang akhirnya masuk ke compile/test/runtime classpath?
1. Posisi Dependency Resolution dalam Kerangka Kaufman
Dalam deconstruction Kaufman, dependency management harus dipecah menjadi beberapa sub-skill:
| Sub-skill | Pertanyaan kunci | Output kemampuan |
|---|---|---|
| Dependency graph reading | Dependency mana yang langsung, mana yang transitive? | Bisa membaca dependency:tree sebagai graph, bukan log |
| Scope reasoning | Dependency ini masuk classpath mana? | Bisa memprediksi compile/test/runtime behavior |
| Conflict mediation | Jika ada dua versi library, Maven pilih yang mana? | Bisa menjelaskan dan memperbaiki version conflict |
| Version governance | Di mana versi harus dikelola? | Bisa mendesain BOM/dependencyManagement |
| Optional/exclusion reasoning | Kapan dependency harus optional atau excluded? | Bisa mengurangi bloat tanpa merusak consumer |
| Convergence enforcement | Bagaimana mencegah graph liar? | Bisa menambahkan guardrail build |
| Diagnostic workflow | Command apa yang harus dijalankan saat konflik? | Bisa self-correct tanpa menebak |
Mental model utama:
Dependency resolution bukan hanya mencari file JAR. Ini adalah proses membentuk graph, memilih versi, dan memfilter classpath.
2. Dependency Graph sebagai Arsitektur Tersembunyi
Setiap dependency yang kita tambahkan adalah edge baru dalam graph. Edge itu membawa konsekuensi:
- API baru yang bisa dipakai developer;
- transitive dependencies baru;
- classpath collision risk;
- CVE exposure;
- license obligation;
- startup/memory impact;
- binary compatibility risk;
- release coordination risk.
Contoh POM sederhana:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.1.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version>
</dependency>
</dependencies>
Yang tampak hanya dua dependency. Namun setiap dependency bisa membawa dependency lain.
Untuk engineer senior, dependency graph adalah bagian dari arsitektur. Ia harus direview seperti kita mereview API, database schema, atau state machine.
3. Direct vs Transitive Dependency
Direct dependency adalah dependency yang ditulis langsung di POM project kita.
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
Transitive dependency adalah dependency yang dibawa oleh dependency lain.
Misalnya:
my-service
└── library-a
└── library-b
└── library-c
Jika my-service hanya mendeklarasikan library-a, maka library-b dan library-c adalah transitive dependencies.
Rule praktis:
Jika source code kita langsung meng-import tipe dari sebuah library, library itu sebaiknya menjadi direct dependency.
Contoh buruk:
import com.fasterxml.jackson.databind.ObjectMapper;
Tetapi pom.xml tidak punya direct dependency ke jackson-databind karena Jackson kebetulan datang dari Spring Boot starter.
Masalahnya:
- source code kita bergantung pada Jackson;
- POM tidak menyatakan dependency itu secara eksplisit;
- jika starter berubah dan tidak lagi membawa Jackson, code kita rusak;
- ownership dependency menjadi kabur.
Lebih baik:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
Jika memakai BOM/platform, version bisa dikelola terpusat, tetapi dependency tetap direct.
4. Maven Dependency Declaration Anatomy
Satu dependency declaration bisa berisi:
<dependency>
<groupId>com.acme.risk</groupId>
<artifactId>risk-engine-client</artifactId>
<version>2.4.1</version>
<type>jar</type>
<classifier>jdk21</classifier>
<scope>compile</scope>
<optional>false</optional>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
Field paling umum:
| Field | Makna |
|---|---|
groupId | Namespace artifact |
artifactId | Nama artifact |
version | Versi artifact, kecuali dikelola dependencyManagement |
type | Jenis artifact, default jar |
classifier | Variasi artifact |
scope | Classpath/filtering semantics |
optional | Apakah dependency ini transitively inherited consumer |
exclusions | Edge transitive mana yang dipotong |
Jangan mengisi semua field hanya agar terlihat lengkap. Semakin banyak deklarasi, semakin besar surface area untuk salah konfigurasi.
5. Scope: Dependency Masuk Classpath Mana?
Maven scope menentukan kapan dependency tersedia.
| Scope | Tersedia saat compile? | Tersedia saat test? | Tersedia saat runtime? | Umum untuk |
|---|---|---|---|---|
compile | Ya | Ya | Ya | Dependency utama/default |
provided | Ya | Ya | Tidak disertakan sebagai runtime dependency aplikasi | Servlet API, container-provided API |
runtime | Tidak untuk compile main | Ya | Ya | JDBC driver, runtime implementation |
test | Tidak | Ya | Tidak untuk runtime aplikasi | JUnit, Mockito, test fixtures |
system | Ya | Ya | Bergantung path lokal | Hindari; tidak portable |
import | Khusus dependencyManagement BOM | Tidak langsung ke classpath | BOM import |
5.1 compile
Default scope.
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.13</version>
</dependency>
Gunakan untuk API/library yang dibutuhkan source utama dan runtime.
5.2 provided
Dependency tersedia saat compile, tetapi diasumsikan disediakan runtime environment.
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
<scope>provided</scope>
</dependency>
Cocok untuk WAR yang berjalan di servlet container. Berbahaya jika dipakai pada executable JAR yang sebenarnya harus membawa semua runtime dependency sendiri.
5.3 runtime
Dependency tidak dibutuhkan untuk compile source utama, tetapi dibutuhkan saat runtime.
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
<scope>runtime</scope>
</dependency>
Cocok untuk JDBC driver jika code hanya bergantung pada java.sql atau abstraction yang tidak membutuhkan tipe driver langsung.
5.4 test
Dependency hanya untuk test.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
Jangan biarkan test helper masuk ke main runtime classpath.
5.5 system
Scope ini menunjuk file lokal via path. Hindari.
<dependency>
<groupId>com.vendor</groupId>
<artifactId>legacy-driver</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/legacy-driver.jar</systemPath>
</dependency>
Ini merusak reproducibility dan repository governance. Lebih baik publish artifact ke repository internal.
5.6 import
Dipakai di dependencyManagement untuk mengimpor BOM.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
import tidak menambahkan dependency ke project. Ia hanya mengimpor versi yang dikelola.
6. Scope Transitivity: Jangan Hanya Pahami Baris Lokal
Scope juga mempengaruhi bagaimana transitive dependency masuk ke downstream project.
Contoh:
app
└── library-a compile
└── library-b runtime
library-b bisa muncul sebagai runtime dependency app, tergantung kombinasi scope.
Poin penting:
- scope bukan hanya label dokumentasi;
- scope mempengaruhi compile/test/runtime classpath;
- scope transitive bisa mengejutkan;
- packaging plugin juga bisa memperlakukan scope berbeda.
Praktik senior:
- Jalankan
dependency:treedengan scope berbeda. - Cek packaged artifact akhir.
- Jangan asumsikan dependency tidak ikut hanya karena tidak direct.
Contoh command:
./mvnw dependency:tree -Dscope=compile
./mvnw dependency:tree -Dscope=runtime
./mvnw dependency:tree -Dscope=test
7. Dependency Mediation: Jika Ada Dua Versi, Maven Pilih Mana?
Dependency graph sering membawa versi berbeda untuk artifact yang sama.
Contoh:
my-service
├── library-a
│ └── commons-codec:commons-codec:1.15
└── library-b
└── commons-codec:commons-codec:1.16
Maven tidak memasukkan dua versi commons-codec ke classpath normal. Maven memilih satu versi melalui dependency mediation.
Rule utama Maven:
- Nearest definition wins — dependency yang jalurnya paling dekat dari root project menang.
- Jika kedalaman sama, first declaration wins — dependency yang lebih dulu dideklarasikan menang.
- Direct dependency dari project root biasanya menang atas transitive dependency karena jaraknya lebih dekat.
Contoh 1 — nearest wins:
my-service
├── commons-codec:commons-codec:1.16 <-- direct, depth 1, wins
└── library-a
└── commons-codec:commons-codec:1.15 <-- transitive, depth 2
Contoh 2 — same depth, first declaration wins:
<dependencies>
<dependency>
<groupId>com.acme</groupId>
<artifactId>library-a</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.acme</groupId>
<artifactId>library-b</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
my-service
├── library-a
│ └── commons-codec:commons-codec:1.15 <-- same depth, first branch wins
└── library-b
└── commons-codec:commons-codec:1.16
Ini powerful tetapi berbahaya. Urutan dependency declaration bisa mengubah resolved version.
Rule enterprise:
Jangan mengandalkan kebetulan nearest-wins untuk dependency penting. Kelola versi secara eksplisit melalui direct dependency atau
dependencyManagement/BOM.
8. dependencyManagement: Mengatur Versi, Bukan Menambahkan Dependency
dependencyManagement memberi aturan versi/scope/default untuk dependency, tetapi tidak otomatis menambahkan dependency ke classpath.
Contoh:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
Dependency di <dependencies> tidak perlu menulis version karena version dikelola di <dependencyManagement>.
Salah kaprah:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
Lalu mengira JUnit sudah tersedia. Belum. Kita tetap perlu:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Mental model:
9. BOM: Bill of Materials
BOM adalah POM khusus yang berisi daftar dependency versions yang dikelola bersama.
Contoh import BOM:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.acme.platform</groupId>
<artifactId>acme-java-platform-bom</artifactId>
<version>2026.06.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Setelah BOM diimport:
<dependencies>
<dependency>
<groupId>com.acme.audit</groupId>
<artifactId>audit-event-contract</artifactId>
</dependency>
</dependencies>
Version datang dari BOM.
BOM berguna untuk:
- menjaga library ecosystem tetap aligned;
- menghindari version declaration tersebar;
- mempercepat security patch rollout;
- menyederhanakan migration wave;
- membuat dependency review lebih terpusat.
BOM buruk jika:
- terlalu besar dan memasukkan semua library organisasi;
- tidak punya ownership;
- tidak punya release notes;
- override sembarangan tanpa testing;
- dipakai untuk menyembunyikan dependency yang sebenarnya tidak dipahami.
10. BOM Layering
Dalam enterprise, satu project bisa mengimpor beberapa BOM.
Contoh:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.acme.platform</groupId>
<artifactId>acme-security-bom</artifactId>
<version>2026.06.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Masalahnya: bagaimana jika dua BOM mengatur artifact yang sama?
Kita perlu policy:
- BOM mana yang paling authoritative?
- Apakah internal BOM boleh override framework BOM?
- Bagaimana testing compatibility dilakukan?
- Apakah override dicatat dalam release notes?
- Siapa owner BOM?
Rule praktis:
Layer BOM dari paling eksternal/framework ke paling internal/platform, lalu dokumentasikan override yang disengaja.
Namun jangan membuat terlalu banyak BOM layer. Setiap layer menambah cognitive cost.
11. Optional Dependencies
Optional dependency berarti dependency tersebut tidak otomatis diwariskan ke consumer.
Misalnya library report-exporter mendukung PDF dan Excel:
<dependencies>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
<optional>true</optional>
</dependency>
</dependencies>
Jika aplikasi memakai report-exporter, dependency optional tidak otomatis ikut. Aplikasi harus menambahkan sendiri jika butuh fitur Excel.
<dependency>
<groupId>com.acme.reporting</groupId>
<artifactId>report-exporter</artifactId>
<version>1.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
Optional cocok untuk:
- library dengan feature adapter optional;
- support banyak backend tetapi consumer hanya butuh satu;
- menghindari dependency berat untuk fitur yang jarang dipakai.
Optional buruk jika:
- dipakai untuk menyembunyikan dependency yang sebenarnya wajib;
- membuat runtime error karena consumer tidak tahu harus menambahkan dependency;
- menggantikan modularisasi yang seharusnya dilakukan.
Design lebih baik:
report-exporter-core
report-exporter-pdf
report-exporter-excel
Daripada satu library besar dengan banyak optional dependency.
12. Exclusions: Memotong Edge Transitive
Exclusion menghapus dependency transitive tertentu dari edge tertentu.
<dependency>
<groupId>com.acme.legacy</groupId>
<artifactId>legacy-client</artifactId>
<version>2.0.0</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
Penting: exclusion berlaku pada dependency edge tersebut, bukan global untuk seluruh POM.
Jika another-lib juga membawa commons-logging, dependency itu masih bisa muncul.
Exclusion cocok untuk:
- mengganti implementation yang tidak diinginkan;
- menghapus dependency bermasalah dari library lama;
- mengurangi collision dengan platform dependency;
- memitigasi CVE sementara dengan replacement yang tested.
Exclusion berbahaya jika:
- dilakukan tanpa memahami kenapa dependency dibutuhkan;
- memotong dependency runtime yang diperlukan;
- menyelesaikan dependency tree tetapi menciptakan
ClassNotFoundException; - dilakukan berulang-ulang tanpa upstream fix.
Rule:
Setiap exclusion harus punya komentar atau ADR kecil: dependency apa yang dipotong, kenapa aman, dan bagaimana diverifikasi.
Contoh:
<dependency>
<groupId>com.acme.legacy</groupId>
<artifactId>legacy-client</artifactId>
<version>2.0.0</version>
<exclusions>
<!-- Excluded because platform standardizes logging via slf4j-api + logback.
Verified by integration test LegacyClientLoggingIT. -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
13. Classpath Conflict: Compile Sukses, Runtime Gagal
Java classpath biasanya hanya memuat satu versi class tertentu berdasarkan urutan classpath. Maven mediation memilih satu artifact version, tetapi tidak menjamin binary compatibility.
Contoh failure:
java.lang.NoSuchMethodError: 'void com.fasterxml.jackson.core.JsonFactory.<init>(...)'
Kemungkinan:
- code dikompilasi dengan versi library tertentu;
- runtime memakai versi lain;
- transitive dependency menurunkan versi;
- packaging/deployment environment membawa library sendiri;
- application server menyediakan library lama.
Maven bisa menunjukkan resolved dependency tree, tetapi runtime environment masih perlu diperiksa.
Diagnostic:
./mvnw dependency:tree -Dincludes=com.fasterxml.jackson.core
./mvnw dependency:tree -Dverbose
Untuk aplikasi yang dipackage ke container/app server, cek juga isi artifact akhir:
jar tf target/my-app.war | grep jackson
jar tf target/my-app.jar | grep jackson
Rule:
Dependency resolution Maven adalah necessary condition untuk classpath sehat, bukan sufficient condition. Deployment packaging dan runtime classloader tetap harus dipahami.
14. Dependency Tree sebagai Alat Debug Utama
Command paling penting:
./mvnw dependency:tree
Filter artifact tertentu:
./mvnw dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
Filter group:
./mvnw dependency:tree -Dincludes=com.fasterxml.jackson.core
Lihat scope tertentu:
./mvnw dependency:tree -Dscope=runtime
Output harus dibaca sebagai graph.
Contoh:
com.acme:payment-service:jar:1.0.0
+- com.acme:payment-core:jar:1.0.0:compile
| \- com.fasterxml.jackson.core:jackson-databind:jar:2.17.1:compile
| +- com.fasterxml.jackson.core:jackson-annotations:jar:2.17.1:compile
| \- com.fasterxml.jackson.core:jackson-core:jar:2.17.1:compile
\- com.acme:legacy-client:jar:2.1.0:compile
\- (com.fasterxml.jackson.core:jackson-databind:jar:2.13.5:compile - omitted for conflict with 2.17.1)
Yang harus dicari:
- dependency direct vs transitive;
- versi yang dipilih;
- versi yang omitted;
- path asal dependency;
- scope dependency;
- dependency yang tidak diharapkan;
- duplicate library families.
15. Effective POM dan Dependency Management Debugging
Jika dependency muncul tanpa version di POM lokal, cari dari mana version-nya datang.
./mvnw help:effective-pom -Doutput=effective-pom.xml
Cari artifact:
grep -n "jackson-databind" effective-pom.xml
Kemungkinan sumber:
- parent POM;
- imported BOM;
- profile aktif;
- current POM dependencyManagement;
- framework parent seperti Spring Boot parent.
Jangan langsung menambahkan version lokal untuk “memperbaiki” masalah. Itu bisa menciptakan override liar.
Better workflow:
- Temukan source of truth version.
- Pahami kenapa version itu dipilih.
- Jika perlu override, lakukan di layer yang benar.
- Tambahkan test/guardrail.
- Catat alasan override.
16. Dependency Convergence
Dependency convergence berarti graph tidak membawa beberapa versi berbeda untuk artifact yang sama.
Contoh tidak converge:
my-service
├── library-a
│ └── jaxen:jaxen:1.1.3
└── library-b
└── jaxen:jaxen:2.0.0
Maven akan memilih satu versi berdasarkan mediation, tetapi graph tetap menunjukkan konflik. Ini bisa aman, bisa juga berbahaya.
Maven Enforcer Plugin punya rule dependencyConvergence untuk menggagalkan build saat versi dependency tidak converge.
Contoh:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<id>enforce-dependency-convergence</id>
<phase>validate</phase>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<dependencyConvergence />
</rules>
</configuration>
</execution>
</executions>
</plugin>
Convergence enforcement bagus untuk library/platform yang harus sangat bersih. Namun pada aplikasi besar, rule ini bisa noisy jika dependency ecosystem kompleks. Gunakan dengan policy yang realistis.
17. Require Upper Bound Dependencies
Rule requireUpperBoundDeps memastikan resolved dependency tidak lebih rendah dari versi tertinggi yang ditemukan dalam transitive dependencies.
Contoh:
my-service
├── library-a
│ └── netty:netty-common:4.1.100
└── library-b
└── netty:netty-common:4.1.108
Jika Maven memilih 4.1.100 karena nearest-wins, padahal ada transitive dependency yang butuh 4.1.108, rule ini bisa menggagalkan build.
Contoh konfigurasi:
<requireUpperBoundDeps />
Rule ini sangat berguna untuk mencegah downgrade diam-diam.
Namun jangan blindly upgrade tanpa memahami compatibility. Versi lebih tinggi tidak selalu aman jika ada breaking change, meskipun semver seharusnya membantu.
18. Version Override: Direct Dependency vs Dependency Management
Ada beberapa cara mengubah versi dependency yang dipilih Maven.
Cara 1 — Direct dependency
<dependencies>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.1</version>
</dependency>
</dependencies>
Karena direct dependency dekat dari root, ia biasanya menang.
Cocok jika source code kita memang menggunakan dependency itu secara langsung.
Cara 2 — Dependency management
<dependencyManagement>
<dependencies>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.1</version>
</dependency>
</dependencies>
</dependencyManagement>
Cocok untuk mengontrol transitive version tanpa menambahkan direct dependency edge baru ke source code.
Cara 3 — BOM/platform update
Update BOM yang menjadi source of truth:
<dependency>
<groupId>com.acme.platform</groupId>
<artifactId>acme-java-platform-bom</artifactId>
<version>2026.07.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Cocok untuk update terkoordinasi banyak project.
Decision rule
| Situasi | Pilihan umum |
|---|---|
| Code kita import API dependency itu | Direct dependency |
| Ingin mengontrol transitive version | dependencyManagement |
| Ingin align banyak dependency dalam ecosystem | BOM |
| Ingin memotong dependency yang tidak boleh ada | Exclusion + replacement/test |
| Ingin temporary security patch | Dependency management override + ADR + test |
19. Snapshot vs Release Dependency
Maven mengenal version release dan snapshot.
Contoh release:
<version>1.4.2</version>
Contoh snapshot:
<version>1.4.3-SNAPSHOT</version>
Snapshot berarti versi mutable selama development. Repository dapat menyimpan timestamped snapshot build di balik versi logical -SNAPSHOT.
Risiko snapshot dependency:
- build tidak reproducible;
- artifact yang sama secara logical bisa berubah;
- CI hari ini dan besok bisa memakai binary berbeda;
- debugging production sulit;
- release artifact bisa tidak immutable jika snapshot bocor.
Rule enterprise:
Release artifact tidak boleh bergantung pada
-SNAPSHOTdependency.
CI bisa mengizinkan snapshot pada feature branch tertentu, tetapi release pipeline harus menolak.
Maven Enforcer bisa membantu melalui rule seperti requireReleaseDeps.
20. Dependency Repositories: Resolution Source
Dependency resolution membutuhkan repository.
Common sources:
- Maven Central;
- corporate repository manager;
- proxy/cache repository;
- internal release repository;
- internal snapshot repository;
- local repository.
Mental model:
Dalam enterprise, sebaiknya developer dan CI tidak langsung resolve ke internet sembarangan. Gunakan repository manager/mirror untuk:
- caching;
- audit;
- allowlist/blocklist;
- security scanning;
- retention policy;
- reproducibility;
- outage isolation.
Namun bagian repository governance akan dibahas lebih dalam di Part 019.
21. Dependency Security: Graph adalah Attack Surface
Setiap transitive dependency memperluas attack surface.
Dependency risk bukan hanya direct dependency. Banyak CVE masuk melalui transitive dependency yang tidak pernah ditulis developer di POM.
Praktik baseline:
./mvnw dependency:tree
./mvnw dependency:tree -Dscope=runtime
Lalu integrasikan dengan:
- SBOM generation;
- vulnerability scanning;
- license scanning;
- dependency update review;
- repository allowlist;
- signature/checksum verification;
- release provenance.
Security detail akan dibahas khusus di Part 020. Untuk sekarang, ingat:
Dependency graph adalah bagian dari threat model.
22. Dependency Bloat
Dependency bloat terjadi ketika project membawa terlalu banyak library untuk tugas yang kecil atau historis.
Gejala:
- startup lambat;
- image besar;
- vulnerability scan noisy;
- classpath conflict sering;
- developer takut upgrade;
- test suite lambat;
- native-image sulit;
- shaded/fat JAR terlalu besar.
Cara audit:
./mvnw dependency:tree
./mvnw dependency:analyze
dependency:analyze bisa membantu menemukan used undeclared dan unused declared dependencies, tetapi jangan dianggap sempurna. Reflection, annotation processing, service loader, runtime loading, dan framework magic bisa membuat hasil perlu interpretasi manusia.
Decision rule:
- hapus dependency yang jelas tidak dipakai;
- jadikan direct dependency jika source code langsung memakai API;
- pisahkan module optional;
- hindari starter besar jika hanya butuh satu library kecil;
- review dependency baru seperti review design.
23. Case Study: Version Conflict yang Terlihat Aman tapi Berbahaya
Misalnya project:
<dependencies>
<dependency>
<groupId>com.acme</groupId>
<artifactId>risk-client</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>com.acme</groupId>
<artifactId>case-client</artifactId>
<version>2.4.0</version>
</dependency>
</dependencies>
Tree:
my-service
├── risk-client:1.8.0
│ └── validation-api:2.0.0
└── case-client:2.4.0
└── validation-api:3.1.0
Maven memilih validation-api:2.0.0 karena risk-client dideklarasikan lebih dulu pada depth yang sama.
Build compile sukses karena code kita tidak langsung memakai fitur baru. Namun saat runtime, case-client memanggil method yang hanya ada di validation-api:3.1.0.
Failure:
java.lang.NoSuchMethodError
Fix buruk:
<!-- blindly added because build failed -->
<dependency>
<groupId>com.acme</groupId>
<artifactId>validation-api</artifactId>
<version>3.1.0</version>
</dependency>
Fix lebih baik:
- Pastikan
risk-clientcompatible denganvalidation-api:3.1.0. - Kelola version di dependencyManagement atau BOM.
- Tambahkan integration test yang memakai kedua client.
- Buat issue ke owner
risk-clientjika metadata dependency-nya outdated. - Tambahkan enforcer rule jika konflik semacam ini sering terjadi.
24. Case Study: Optional Dependency yang Salah
Library:
<dependency>
<groupId>com.acme.crypto</groupId>
<artifactId>crypto-provider</artifactId>
<version>1.2.0</version>
<optional>true</optional>
</dependency>
Code library:
import com.acme.crypto.CryptoProvider;
public final class TokenSigner {
private final CryptoProvider provider;
}
Ini buruk jika TokenSigner adalah API utama library. Consumer akan compile/run gagal kecuali menambahkan dependency optional secara eksplisit.
Lebih baik:
- jika crypto-provider wajib, jangan optional;
- jika crypto-provider optional, pisahkan adapter:
token-core
token-crypto-provider-adapter
Optional dependency harus merepresentasikan fitur opsional, bukan dependency wajib yang disembunyikan.
25. Case Study: Exclusion yang Memperbaiki Tree tapi Merusak Runtime
Project mengecualikan dependency:
<dependency>
<groupId>com.vendor</groupId>
<artifactId>vendor-sdk</artifactId>
<version>5.0.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
</exclusions>
</dependency>
Alasan: vulnerability scanner menandai httpclient lama.
Build sukses. Runtime gagal:
ClassNotFoundException: org.apache.http.client.HttpClient
Fix yang benar bukan sekadar exclusion. Kita perlu replacement compatible:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
Atau upgrade vendor-sdk ke versi yang sudah memperbaiki dependency.
Exclusion tanpa replacement/test adalah debt.
26. Diagnostic Workflow Dependency Conflict
Saat ada dependency problem, gunakan workflow berikut.
Step 1 — Identifikasi artifact bermasalah
Dari error:
NoSuchMethodError: com.fasterxml.jackson.databind.ObjectMapper.readerFor
Artifact family kemungkinan:
com.fasterxml.jackson.core
Step 2 — Tampilkan dependency tree terfilter
./mvnw dependency:tree -Dincludes=com.fasterxml.jackson.core
Step 3 — Cek runtime scope
./mvnw dependency:tree -Dscope=runtime -Dincludes=com.fasterxml.jackson.core
Step 4 — Cek effective POM
./mvnw help:effective-pom -Doutput=effective-pom.xml
Cari BOM atau dependencyManagement yang mengatur version.
Step 5 — Tentukan source of truth
Apakah versi seharusnya datang dari:
- framework BOM?
- internal platform BOM?
- parent POM?
- direct dependency?
- emergency override?
Step 6 — Fix di layer yang benar
- direct dependency jika code memakai API langsung;
- dependencyManagement jika mengontrol transitive version;
- BOM update jika ecosystem perlu aligned;
- exclusion jika edge tertentu harus dipotong;
- upstream dependency update jika metadata library salah.
Step 7 — Tambahkan guardrail
- integration test;
- enforcer rule;
- dependency lock/report;
- SBOM diff;
- CI check.
27. Enterprise Dependency Policy
Untuk sistem besar, dependency policy harus eksplisit.
Contoh policy:
- Semua direct library yang dipakai source code harus dideklarasikan langsung.
- Version untuk dependency umum dikelola BOM/platform.
- Release artifact tidak boleh bergantung pada snapshot.
- Dependency baru memerlukan owner dan alasan.
- Exclusion harus punya komentar/ADR.
- Dependency dengan CVE high/critical harus punya remediation plan.
- Test dependency tidak boleh bocor ke runtime artifact.
- Internal library harus dipublish ke repository manager, bukan dibagi via file JAR.
- Build CI harus memakai repository mirror corporate.
- Dependency tree harus bisa direproduksi di CI bersih.
Contoh checklist review dependency baru:
| Pertanyaan | Ya/Tidak |
|---|---|
| Apakah dependency ini benar-benar diperlukan? | |
| Apakah source code memakai API-nya langsung? | |
| Apakah ada alternatif JDK/platform yang sudah ada? | |
| Apakah transitive dependency-nya masuk akal? | |
| Apakah license sesuai? | |
| Apakah ada CVE known? | |
| Apakah version dikelola BOM? | |
| Apakah dependency ini runtime-critical? | |
| Siapa owner upgrade-nya? | |
| Bagaimana rollback jika dependency bermasalah? |
28. Maven Dependency Anti-Patterns
28.1 Mengandalkan transitive dependency sebagai API langsung
Buruk:
import org.apache.commons.lang3.StringUtils;
Tapi commons-lang3 tidak direct di POM.
Fix: declare direct dependency.
28.2 Menambahkan version lokal yang melawan BOM
Buruk:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
Padahal platform BOM mengatur Jackson 2.17.x.
Fix: hilangkan local version atau override di layer governance dengan alasan jelas.
28.3 Exclusion shotgun
Buruk:
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
Atau exclusion banyak dependency tanpa verifikasi.
Fix: potong edge spesifik dan test runtime behavior.
28.4 Snapshot dependency dalam release
Buruk:
<dependency>
<groupId>com.acme</groupId>
<artifactId>case-client</artifactId>
<version>2.5.0-SNAPSHOT</version>
</dependency>
Fix: release dependency dulu, lalu gunakan version immutable.
28.5 systemPath
Buruk:
<scope>system</scope>
<systemPath>${project.basedir}/lib/vendor.jar</systemPath>
Fix: publish vendor JAR ke internal repository dengan coordinates jelas.
29. Practice: Build a Dependency Lab
Buat tiga module kecil:
dependency-lab/
├── pom.xml
├── app/
├── library-a/
└── library-b/
library-a bergantung pada commons-codec:1.15.
library-b bergantung pada commons-codec:1.16.
app bergantung pada library-a dan library-b.
Latihan:
- Jalankan
dependency:tree. - Tukar urutan dependency
library-adanlibrary-bdiapp. - Amati versi
commons-codecyang dipilih. - Tambahkan direct dependency ke
commons-codec:1.16. - Pindahkan version ke
dependencyManagement. - Tambahkan
dependencyConvergencerule. - Dokumentasikan hasil.
Tujuannya bukan belajar commons-codec, tetapi merasakan dependency mediation secara langsung.
30. Practice: Scope Lab
Buat dependency dengan scope berbeda:
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
<scope>runtime</scope>
</dependency>
Lalu coba import class driver langsung di source utama.
Prediksi:
- compile gagal jika source utama membutuhkan type yang hanya ada di runtime dependency;
- test mungkin punya behavior berbeda karena runtime dependency tersedia di test/runtime path.
Ubah scope ke compile, lalu amati perubahan.
Latihan ini mengajarkan bahwa scope bukan formalitas.
31. Practice: Optional Dependency Lab
Buat module:
exporter-core
exporter-excel
app
Versi 1: exporter-core punya optional dependency ke Apache POI.
Versi 2: exporter-excel memisahkan adapter Excel.
Bandingkan:
- dependency tree app;
- clarity API;
- runtime failure risk;
- consumer experience;
- modularity.
Kesimpulan yang biasanya muncul: optional dependency bisa berguna, tetapi modularisasi sering lebih jelas.
32. Checklist Self-Assessment
Sebelum lanjut ke Maven multi-module, pastikan kita bisa menjawab:
- Apa beda direct dan transitive dependency?
- Kapan dependency harus direct walaupun sudah transitive?
- Bagaimana Maven memilih versi saat conflict?
- Apa itu nearest definition?
- Apa arti first declaration wins?
- Apa beda
dependencyManagementdandependencies? - Apa itu BOM dan mengapa
scope=importhanya dipakai di dependencyManagement? - Kapan memakai
provided,runtime, dantest? - Kapan optional dependency masuk akal?
- Mengapa exclusion harus hati-hati?
- Bagaimana membaca
dependency:tree? - Bagaimana mencegah snapshot dependency masuk release?
- Apa risiko dependency bloat?
Jika kita bisa menjawab ini dengan contoh nyata, kita sudah melewati level pemakai Maven dasar.
33. Ringkasan
Maven dependency resolution adalah proses membentuk dependency graph, menerapkan scope, optional/exclusion rule, memilih versi lewat mediation, lalu menghasilkan classpath untuk compile, test, dan runtime.
Konsep utama:
- Dependency graph adalah arsitektur tersembunyi.
- Direct dependency harus mencerminkan API yang dipakai source code.
- Scope menentukan classpath availability.
- Maven memilih versi dengan nearest definition, lalu first declaration wins jika depth sama.
dependencyManagementmengatur versi, bukan menambahkan dependency.- BOM membantu alignment lintas library ecosystem.
- Optional dependency harus dipakai hati-hati; modularisasi sering lebih baik.
- Exclusion memotong edge spesifik dan harus diverifikasi runtime.
- Convergence dan upper-bound rules bisa menjadi guardrail.
- Snapshot dependency tidak boleh bocor ke release artifact.
Part berikutnya akan membahas Maven multi-module enterprise builds: parent POM, aggregator, reactor, module ordering, partial builds, corporate parent, dan trade-off multi-module di sistem besar.
Referensi Faktual Utama
- Apache Maven — Introduction to the Dependency Mechanism: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html
- Apache Maven — Optional Dependencies and Dependency Exclusions: https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html
- Apache Maven Dependency Plugin — Usage: https://maven.apache.org/plugins/maven-dependency-plugin/usage.html
- Apache Maven Enforcer — Dependency Convergence: https://maven.apache.org/enforcer/enforcer-rules/dependencyConvergence.html
- Apache Maven Enforcer — Require Upper Bound Dependencies: https://maven.apache.org/enforcer/enforcer-rules/requireUpperBoundDeps.html
You just completed lesson 08 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.