I recently came across this excellent article on dependency management in Ruby. It rightly states that the concerns are language agnostic and I really think it’s worth covering specifically for Java and Maven as so many people repeatedly get it wrong.
I’ve spent most of my career working in very large organisations with cross-cutting concerns. Any given project will have a dependence on around 4 or so other systems, give or take. For a variety of reasons (poor/no guidance, no architects who can code) this interdependence has been implemented in a myriad of ways, but with a surprising reliance on client libraries. Whether this is a good or bad thing (it’s a bad thing) is for a whole other post, but I’ve had the pleasure of suffering first hand due to poor dependency management in client libraries. In this post, I’m going to cover some important do’s and don’ts. Whether you’re making a client library, a util library, or anything which someone else will be relying on, please follow these rules. They will make you liked and loved by other teams.
Big open caveat up front; I’ve made a number of these mistakes myself earlier in my career. I hope you can learn from my errors.
Make it Tiny: Aim for Zero Dependencies
The best client library is one that depends on nothing else. This way the consumer faces no risk of clashing libraries and versions. You should go to relatively extreme distances to achieve this.
There are a bunch of helper libraries people have as go-to's - Guava and Apache Collections are two of the more common ones I see. I know you may find them really useful and the idea of coding without them is crazy, but don’t do it. Every consumer of your library is going to have to bring in the entirety of that dependency into their code base just because you want a simple way to find the differences in a list. This is really bad. This gets worse when the consumer is operating on a newer version of the library than you and there are breaking API changes. They may be forced to downgrade and use and old version because you don’t keep your libraries up to date.
If you simply cannot live without certain functions from a dependency (but it’s still a minority of the library), copy the code out of the library and include it in your codebase. It’s controversial as you will have to manually upgrade it if the dependency changes, but if it’s something stable then it probably won’t need to happen. Just make sure you change the package space, and maybe even the class name, so there are no clashes if the consumers use the same library.
For the Love of God Don’t Include Spring
This is one of the cases I’m very guilty of. One of my first tasks as a junior dev was to create a client/server comms layer running over ActiveMQ. I was deep into my Spring phase and every single library I created for other dev included Spring and mandated the use of Spring to use it.
I was a very junior developer.
Firstly, your library should in no way mandate or influence how the consumer wires up their application. If they want to use Spring, Guice, or manually wire it then it’s dealers choice. You are creating a library. Provide functions, don’t override the consumers choice.
Secondly, Spring causes dependency mismatch hell. It has breaking changes between 2, 3 and 4, and differing behaviour on sub-point releases. I’ve seen a project where they had to create a separate component (a Martin Fowler simplification) to handle a client library because they were unable to resolve the clashes between spring 2.5.6 and spring 3.0.1. This is insane.
This also very clearly applies to other libraries than Spring, it just happens I have the most experience with Spring pain. I refer you to point 1- aim for zero dependencies.
Use the Maven Dependency System: Do Not Use Fat Jars
One of the benefits of Maven is that a consumer can choose to exclude your dependencies. If you’ve built your library well it hopefully won’t be needed, but you should always aim to give the consumer the most choice, whether that’s ignoring or overriding your dependencies. If you choose to use a far jar (that is, your project has the jars and/or classes for all of the things you depend on) then you are forcing the client to use, and be stuck on, whatever you’ve packaged.
At my last role, a fairly important central team had done this. They’d copied and pasted the code from Spring and Apache Collections and a bunch of other stuff into their Jar, and hadn’t changed the package space. This invariably leads to clashes and problems for all of the consumers, exacerbated by the fact that it was never updated and had libraries that were years out of date.
All the major dependency management systems such as gradle are compatible with Maven. Use it properly.
Only depend on things you need
It's really easy to leave dependencies in that you don't need anymore. It's hard to figure out if a dependency is still valid. Fortunately, there's a tool for that- The Maven Dependency Analyzer. If you stick this into your build it will log out to tell you exactly which dependencies you've got which you don't need, along with dependencies you need that you haven't explicitly got (and are liable to cause your system to break if your dependencies change).
Logging out isn't always enough though; throw in the Maven Enforcer Plugin on your build and if your dependencies are wrong then the build will fail. Beautiful!