Maven SNAPSHOT — Why CI Builds Pass Locally But Fail
A CI-only ClassNotFoundException from a breaking SNAPSHOT API change while local builds pass.
- Maven is a declarative build tool and dependency manager for Java — you describe what you need in pom.xml and it handles the rest
- GAV coordinates (GroupId, ArtifactId, Version) uniquely identify every artifact in the Maven ecosystem
- The standard directory layout (src/main/java, src/test/java) is mandatory by convention — Maven ignores files placed elsewhere
- Build lifecycles are ordered phases: running 'mvn package' automatically triggers compile and test first
- Transitive dependencies are resolved automatically — Library A's dependency on Library B is pulled in without you declaring it
- The biggest trap: hardcoding local file paths in pom.xml breaks the build for every teammate and every CI server
Think of Maven as one of the most quietly powerful decisions you make at the start of a Java project. Most beginners don't realize it until they've spent a Friday afternoon manually chasing down mismatched JAR versions — then Maven suddenly makes a lot of sense.
Here's the mental model I keep coming back to: imagine you're a head chef running a high-end kitchen. Without Maven, you drive to five different markets to track down specific spices, wash every dish by hand, and rewrite the recipe from scratch for every new cook who joins your team. Maven is the combination of a world-class supply chain and an automated prep kitchen. You write down the ingredients you need in a master book — the pom.xml — and it sources them, preps the station, and produces the same dish every single time regardless of which kitchen you're standing in.
The part most tutorials skip: Maven isn't magic. It's a very opinionated contract. Follow its conventions and it practically runs itself. Break them and you'll spend more time fighting configuration than writing code.
Apache Maven has been part of the Java ecosystem since 2004, and if you've worked on more than one Java project professionally, you've almost certainly encountered it — even if you didn't realize you were looking at it. It shows up as that pom.xml file in the root of the repository that nobody wants to touch.
But Maven is worth understanding properly, not just cargo-culting from project to project. It is a project management and build comprehension tool that gives every Java project a uniform structure, a consistent build process, and a declarative way to express external dependencies. When it works correctly — which is most of the time — it's invisible. When it breaks, and it will break, knowing what's actually happening underneath saves you hours.
This guide is written from the perspective of someone who has debugged Maven builds at 11pm before a production release, reviewed dozens of pom.xml files in code reviews, and watched teams make the same five mistakes across completely different organizations. We'll cover what Maven is, the mental models you need to reason about it correctly, real failure modes from production environments, and the practical mechanics of pom.xml, lifecycles, and dependency management.
By the end, you'll have both the conceptual foundation and the hands-on examples to use Maven with confidence — and more importantly, to debug it when something goes wrong.
What Is Maven and Why Does It Exist?
Before Maven, every Java project was its own small build systems problem. One team used Ant scripts with manually maintained lib/ folders full of JARs checked into version control. Another team used shell scripts that only worked on specific operating systems. A third team had a README with 14 manual steps that was already three months out of date. Adding a new developer to any of these projects meant a half-day of setup friction at minimum.
Apache Maven was created to solve build fragmentation across the entire Java ecosystem with a single architectural decision: Convention over Configuration. Instead of each project defining how to build itself, Maven defines one correct way to build any Java project. Your source code goes in src/main/java. Your tests go in src/test/java. Your compiled output goes to target/. Your project is described in pom.xml using a declarative format that says what you need, not how to produce it.
The consequence of this is significant: a developer who has never seen your project before can run 'mvn clean install' and get a working build. No README archaeology required. No environment-specific setup scripts. CI/CD systems like Jenkins and GitHub Actions rely on exactly this contract — they know how to build Maven projects without per-project configuration because Maven projects all work the same way.
The other half of what Maven does is dependency management. Before centralized repositories, you literally emailed people for JAR files or downloaded them from project websites. Maven Central changed that. You declare a dependency by its GAV coordinates in pom.xml, and Maven fetches it — along with everything that dependency needs — automatically. The transitive dependency resolution that feels obvious today was genuinely novel when Maven introduced it.
- Standard directory layout is non-negotiable for zero-config builds: src/main/java for source, src/test/java for tests, src/main/resources for non-Java assets — Maven scans exactly these paths and nowhere else
- pom.xml is declarative, not procedural: you list what you need, Maven's plugins decide how to produce it — this is why pom.xml is readable by someone who has never run the build
- The Super POM is Maven's hidden foundation: every pom.xml silently inherits compiler settings, plugin versions, and repository URLs from a built-in parent — it's why a four-line pom.xml can compile a project at all
- Lifecycle phases are ordered and cumulative: 'mvn package' always runs validate, compile, and test first — you cannot run package and skip compile, by design
- Overriding conventions is always possible but always has a cost: you must document every override, because the next engineer to read your pom.xml will assume conventions unless told otherwise
Common Mistakes and How to Avoid Them
After reviewing a few hundred pom.xml files across different teams and organizations, the failure patterns are remarkably consistent. The same five or six mistakes appear regardless of team size, company, or project domain. Understanding them in advance is worth more than discovering them through production incidents.
The most frequent mistake is misunderstanding what 'Convention over Configuration' actually means in practice. It means Maven has already decided where your code lives, how it gets compiled, and what gets packaged. If you put your Java files in the wrong directory — even by one level — Maven compiles successfully and produces an empty JAR. It does not warn you. The build is green and the artifact is wrong. This particular failure mode is subtle enough to ship to CI before anyone notices.
The second most common mistake is dependency scope confusion. A dependency without an explicit <scope> defaults to compile scope — it is present at compile time, test time, and it ships inside your final artifact. Test frameworks (JUnit, Mockito, TestContainers) must be declared with <scope>test</scope> or they end up in your production JAR, inflating its size and adding transitive dependencies you don't need in production. The same logic applies to <scope>provided</scope> for libraries supplied by the runtime container — if your application runs in Tomcat, the Servlet API should be provided-scoped, not compile-scoped.
Transitive dependency conflicts are the third pattern, and they're the most insidious because they often don't surface during compilation. They appear at runtime as NoSuchMethodError or ClassNotFoundException when the JVM loads a class from the wrong version of a library. The diagnosis requires 'mvn dependency:tree -Dverbose' and some patience reading the output.
Snapshot dependency breaks CI — builds pass locally but fail on Jenkins
- SNAPSHOT versions are mutable by contract — the same version string can and will resolve to different JARs over time, sometimes within hours
- Never use SNAPSHOT dependencies on release or main branches — pin to release versions without exception
- Local ~/.m2/repository caches artifacts indefinitely by default, which masks dependency issues that are immediately visible on a clean CI agent
- When a build fails on CI but not locally, check the dependency tree before assuming infrastructure problems — the answer is usually in the pom.xml
- Add dependency tree diffing to your CI pipeline so dependency drift is caught automatically rather than discovered during an incident
Key takeaways
Common mistakes to avoid
6 patternsOverusing Maven for trivial projects
Not understanding Maven lifecycle ordering
Ignoring warnings in Maven build output
Hardcoding local file system paths in pom.xml
Using SNAPSHOT dependencies in production or release branches
Not using dependencyManagement in multi-module projects
Interview Questions on This Topic
What is the Project Object Model (POM) and why is it considered the 'unit of work' in Maven?
Frequently Asked Questions
That's Build Tools. Mark it forged?
3 min read · try the examples if you haven't