As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Managing dependencies in a Java project can feel like trying to organize a toolbox that keeps growing and changing on its own. One day, everything fits nicely. The next, you've got three different versions of the same hammer, a missing screwdriver, and a mysterious, possibly insecure, wrench you don't remember adding. Maven and Gradle are the systems we use to bring order to this chaos. They don't just fetch libraries for us; they help us control exactly what gets into our build, ensuring it's stable, fast, and secure. Today, I want to walk you through five practical methods I use to take control of this process.
Let's start with a common headache: the unpredictable build. Your code works perfectly on your laptop, but when your colleague checks it out or when it runs on the continuous integration server, it breaks. Often, this is because a library you depend on, or a library that your library depends on, resolved to a slightly different version elsewhere. This is where the first technique comes in.
We can create a locked snapshot of every single library version in our dependency graph. Think of it as taking a precise inventory of every tool in your toolbox and its exact model number. Both Maven and Gradle can generate a lockfile. This file doesn't contain the libraries themselves, but a definitive list of which version of every library was successfully used in a build.
In Gradle, you enable this feature and then generate the file. Once the lockfile is generated and committed to your project, any future build will use only the versions listed there, regardless of what newer versions might be available. This makes your builds perfectly repeatable across any machine or environment.
// In your build.gradle file, enable locking for all configurations
dependencyLocking {
lockAllConfigurations()
}
// To generate the lockfiles, run this command in your terminal
// ./gradlew dependencies --write-locks
// This creates files like `gradle.lockfile` for each configuration.
// Here's a simplified look at what's inside one:
// com.google.guava:guava:31.0.1-jre=compileClasspath, runtimeClasspath
// org.junit:junit-bom:5.8.2=testCompileClasspath, testRuntimeClasspath
For Maven, a similar outcome is achieved using the maven-enforcer-plugin with its requireSameVersion rule, or more directly with the lock-maven-plugin. The lock plugin creates a pom-lock.xml file. The principle is identical: pin everything in place.
The second technique deals with a sneaky problem: transitive dependencies. When you declare you need Library A, it might say it needs Libraries B and C to work. Gradle and Maven automatically bring those in for you. But what if Library B is large and you don't use it, or it conflicts with another Library B version you explicitly want? You need a way to say, "Bring me Library A, but leave its friend Library B out of my toolbox."
This is called an exclusion. You surgically remove a specific transitive dependency from the graph. In Maven, you do this right inside the <dependency> tag.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- This starter includes an embedded Tomcat server by default -->
<exclusions>
<!-- But I'm deploying to a standalone server, so I don't need it -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
In Gradle, the syntax is a bit different but just as powerful. You can exclude a module by its group, name, or both.
implementation('org.springframework.boot:spring-boot-starter-web') {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}
This keeps your final application artifact lean and prevents two versions of the same library from fighting each other.
Sometimes, exclusions aren't enough. You need to lay down the law for your entire project. This is our third technique: defining custom resolution rules. It’s like setting house rules for your build. You can force a specific version of a library, replace one dependency with another, or even fail the build if a conflict is detected.
In Gradle, you use the resolutionStrategy block. I use this all the time to enforce a single version of a library across all my dependencies, especially for common things like logging frameworks or JSON parsers where multiple versions cause subtle bugs.
configurations.all {
resolutionStrategy {
// Option 1: Be strict. Fail the build if two dependencies bring in conflicting versions.
failOnVersionConflict()
// Option 2: Force a specific version everywhere.
force 'com.fasterxml.jackson.core:jackson-databind:2.13.3'
// Option 3: Dynamically change versions for a whole group of libraries.
eachDependency { details ->
if (details.requested.group == 'org.apache.logging.log4j') {
// Ensure we use a secure, recent version of all Log4j components
details.useVersion '2.17.1'
}
}
}
}
Maven handles this through the <dependencyManagement> section in a parent POM or through BOM (Bill of Materials) imports. It’s a more declarative way to centralize version definitions. When you declare a version in dependencyManagement, it becomes the default for any dependency matching that group and artifact ID in your project, overriding what transitive dependencies might request.
Our fourth technique is about where we get our tools from. By default, Maven and Gradle look at the public Maven Central repository. But in the real world, you'll also use company-private repositories, maybe legacy in-house libraries, or other public repos like JCenter (though it's being sunset). Configuring these repositories properly is crucial for reliability, speed, and access control.
A good repository setup defines a clear order and specifies where to look for what. You typically want to check your local cache first, then your private company repository (which might proxy public ones), and finally the public repositories. Here’s a Gradle example that mirrors a common enterprise setup.
repositories {
// 1. Local Maven cache on your machine (~/.m2/repository). Fastest.
mavenLocal()
// 2. Company Artifactory/Nexus. Hosts private libs and caches public ones.
maven {
url 'https://artifactory.mycompany.com/repository/public-all/'
credentials {
// Credentials should come from gradle.properties, not hardcoded.
username = project.findProperty('artifactoryUser') ?: 'guest'
password = project.findProperty('artifactoryPassword') ?: ''
}
// Tells Gradle what kind of metadata to expect from this repo
metadataSources {
mavenPom()
artifact()
}
}
// 3. Public Maven Central as a last resort.
mavenCentral()
}
The order matters. If your company repo has a patched version of a library, you want it to be found before the public, unpatched version. Authentication is also handled here, keeping sensitive credentials out of your build script.
Finally, we come to a non-negotiable technique for modern development: checking your tools for known flaws. You wouldn't use a ladder with a cracked rung. Similarly, you shouldn't bundle libraries with known security vulnerabilities into your application. The fifth technique integrates vulnerability scanning directly into your build process.
Plugins can analyze your entire dependency tree against databases of known Common Vulnerabilities and Exposures (CVEs). They can break your build if a severe vulnerability is found, giving you a clear signal to update or replace that library.
In Maven, the OWASP Dependency-Check plugin is a standard choice. You configure a severity threshold; if a vulnerability scores above that, the build fails.
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>7.4.4</version>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- Fail the build for vulnerabilities rated 7 or higher (on a 0-10 scale) -->
<failBuildOnCVSS>7</failBuildOnCVSS>
<!-- A file where you can list false positives or accepted risks -->
<suppressionFile>${project.basedir}/security-suppressions.xml</suppressionFile>
</configuration>
</plugin>
Running mvn verify would now include this security check. For Gradle, there is an equivalent dependency-check-gradle plugin with similar configuration. I run this check as part of my continuous integration pipeline. It’s not uncommon for the scan to flag something, and while it’s sometimes a false alarm, it often uncovers a real issue that needs to be patched. This turns a potential security incident into a routine maintenance task.
Putting it all together, managing dependencies isn't a one-time setup. It's an ongoing practice. You start with a locked, reproducible build. You trim the fat with exclusions to avoid bloat and conflict. You establish firm rules to govern versions across the board. You define clear, secure sources for all your artifacts. And you regularly scan everything for weaknesses.
When I apply these methods, my builds become less of a mystery and more of a reliable engine. The "it works on my machine" problem fades away. Deployment surprises are reduced. I sleep a little better knowing a known critical vulnerability won't slip into production because my build will literally refuse to create it. It turns dependency management from a source of frustration into a cornerstone of project stability. It’s the difference between a pile of parts and a well-maintained, trustworthy machine.
📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (1)
It does not make sense to create
lockfiles.. because thepom.xmlalready contains the version you need .... in cases where you use SNAPSHOT`s.. it's usually not really necessary ... and btw. you can use versions maven plugin to resolve those SNAPSHOT to time based versions for a short time period. In general lock files are superflouus in Maven even with SNAPSHOT's...