For what it's worth, I no longer agree with this issue that I started 6 years ago! I have a best-practice document which I apply to my projects and have pasted here in this comment. Under this strategy, I always declare compile-time dependencies in any project that has an import; statement referring to the class. I don't rely on transitive dependencies when I'm actually using a dependency for compilation. I think this is what the Maven authors intended, which is why "provided" has stayed like it is for such a long time.
1. <dependencyManagement/> in parent pom
Declare the versions for all dependencies in the project's parent pom using the <dependencyManagement/> section. You're setting the versions that will be used if the submodules decide to declare dependencies. Using dependency management ensures that the version of all of your dependencies is entirely consistent across all your sub-projects. This is especially important if you're creating a single deployment consisting of many of these sub-modules and you are going to deploy one set of dependency jars
Declare few or no dependencies in the <dependencies/> section of the parent pom. It's fair to assume that all your projects will depend on JUnit and a logging framework so you may want to add those. Anything else results in unnecessary dependencies making their way into your sub-projects.
It's not too useful to declare versions at an even higher level (e.g. the pom that's the parent of all your projects) but in some cases it can make life easier.
Failing to add a dependencyManagement section results in a classic problem of scripts being generated in sub-projects which reference jar versions that don't end up being packaged in the final assemby's lib folder.
2. Declare dependencies where you use them - don't rely on transitive dependencies
Transitive dependencies are very useful. If you're using the public API of a library in your code then you shouldn't have to care how its internals work. Your sub-project should declare a dependency on the library that your code is using; Maven will helpfully pull in the library's transitive dependencies. The library is free to change how its internals work and what transitive dependencies it declares for each version.
If you're using a library, you should declare it in the <dependencies/> section of your pom. Don't rely on transitive dependencies. If your Java code (or Spring config; that's code too) uses commons-beanutils then you should declare that dependency. If you don't, your build is brittle; internal changes to other components can break your component's build.
This tool will tell you about artifacts that you've failed to declare dependencies for and will also tell you if you have extra dependencies declared that you don't need. Be careful about dependencies that you don't use in your .java files; the dependency plugin is only scanning those. If you use a class in Spring config then you do really need it, even if the dependency plugin is telling you that you don't. In these cases I find it useful to make sure that the dependency is set to <scope>runtime</scope>. That way, when I get the dependency:analyze report telling me that these are extra dependencies I know that I probably set them to runtime because they're used by spring config. I ignore the runtime dependencies and only weed out the compile scope ones.
3. Use <properties/> sparingly (in the parent pom)
Declaring all dependency versions in properties and then using those properties in the <dependencyManagement/> section is a little like the (bad) practice of unnecessarily declaring all your Java variables right at the top of a method.
When there's a single artifact to configure, favour hardcoding the version into the <version>1.0</version> tag.
Use properties when there are multiple artifacts to control. For example, set:
and then use:
for artifacts like <artifactId>spring-core</artifactId>
Properties are public, not private. When you declare a property in the parent pom, it's available anywhere in the build. Adding a property pollutes the (build) environment.
4. Use the enforcer plugin to guard against old or unwanted dependencies
The enforcer plugin can fail your build based on duplicate classes found (the same classes in more than one jar file on the build classpath) or can allow you to ban old or unwanted dependencies. This is especially useful when the owner of a library has changed the groupId or artifactId of their artifact. This stops the Maven dependency mechanism from picking the latest version; you'll end up with 2 similar artifacts on the classpath. For example, from Spring 2 to Spring 3 they changed an artifactId from spring to spring-core. Without banning the older spring dependency, it can easily transitively creep back onto your classpath. Then when your program runs it's somewhat pot luck as to which jar is first on the classpath and therefore which version of a class gets loaded.
5. Detect whether you're using old plugin or dependency versions
The following goals of the versions plugin can help you keep up to date with the latest dependencies. Ideally, get your continuous integration server to display this information.