CI/CD setup for Java applications (Maven/Gradle)

Manual build of Java projects consumes hours of team time, and errors surface only in production. We set up CI/CD for Java turnkey: build, automated tests, and release without manual steps. Build time is cut in half, and the number of rollbacks is reduced threefold.

Our competencies:

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1365
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1330
  • B2B Advance company logo design
    B2B Advance company logo design
    776
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1077
  • AIDER company logo development
    AIDER company logo development
    1022
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1113

Building Java projects in CI/CD: where it breaks and why

Building Java projects breaks not at the moment of commit, but at the junction between “it works on my machine” and “it won’t come up on staging.” The reason is almost always the same — environment divergence: developers have different JDK versions, their own Maven or Gradle settings, their own set of dependencies in the local cache.

The same source code then builds into different artifacts, and reproducing the failure on someone else’s machine is no longer possible.

Until this divergence is eliminated, manual steps live between build and deployment: someone edits configuration directly on the server, copies the artifact by hand, quickly finishes building a module.

Such a build failure is not reproducible for everyone and not always, and time to recovery grows with the number of manual operations.

Separate build bottlenecks are tests and static checks run after artifact publication or only on the release branch. The defect is found when the build has already spread across environments, and the release shifts by a whole fix cycle. Checks in the wrong place give a false sense of a green build.

We look at how this is closed: we pin tool and dependency versions in the project definition, move manual deployment into automated steps, and place checks so that they stop the build before artifact publication.

The effect of a well-tuned build: time, stability, rollbacks

The effect is measured by four metrics that we capture before work begins and repeat after: build time, share of successful builds, deployment frequency, and time to restore the previous version.

Without these measurements, talk of “it has become more convenient” is unsupported — and it is precisely they that show where the bottleneck is: in the build itself, in checks, or in manual deployment steps.

The first thing that delivers effect is dependency caching and a single build image.

While modules are pulled from external repositories on every build, time grows with the number of modules; when the local artifact repository is warmed up, the build takes minutes and almost does not depend on project size.

Parallel execution of modules and tests shortens the path from code change to built artifact even more, and dependency versions are pinned in the manifest — the same set of inputs produces the same result on the developer’s machine and on the build node.

Repeatability is what directly affects the share of successful builds.

If the environment is defined declaratively rather than assembled by hand on a specific server, “it works locally for me” stops being a separate class of problems: Gradle or Maven in a container with a pinned JDK version produces an identical artifact, and version divergence is caught by matrix builds, not on staging before release.

The share of deployments without manual intervention grows along with this — the typical change path is covered by a set of checks, and a human is needed only to approve the release.

Deployment frequency and recovery time are two sides of the same decision. While a release is a multi-hour event, change batches are large and the risk is high; when deployment is automated, a small change goes to production and is easy to trace.

Rollback to the previous version relies on the same artifact marked with a tag: instead of rebuilding the old branch, staging gets a ready image, and recovery time is measured in minutes rather than searching for “what exactly broke.”

Comparison of build modes and check execution

The build mode determines whether the result is reproducible and what happens when tasks run simultaneously. Local builds, a shared server, and isolated runners are not a “worse–better” scale, but different levels of isolation with different maintenance costs.

The choice depends on how many branches are built in parallel, whether a shared dependency cache is needed, and how critical it is that one build does not affect another. Below is a comparison across three modes.

Build mode When it fits Limitations
Local build Debugging, single projects, checking changes before pushing to a shared repository The result depends on the machine: JDK version, contents of the local cache, set of plugins. Divergence from the shared environment is discovered only after pushing
Shared build server One or two active branches, a small team, short Maven or Gradle builds Tasks share one directory and one cache: parallel runs compete for resources, and one task’s state leaks into another
Isolated runners per task Many branches and changes at once, different JDK versions, requirement for a reproducible result Each runner brings up the environment from scratch, so the dependency cache must be externalized, otherwise builds slow down

In practice, modes are combined: isolated runners per task plus a shared dependency cache between them. Then parallel builds do not interfere with each other, and environment preparation takes seconds, not minutes.

Stages of setting up CI/CD for Java applications

The order of work almost does not depend on whether the project uses Maven or Gradle — only build targets and dependency caching differ.

We start with an inventory: what is built on developers’ machines, which tests actually run, and by which manual actions a release reaches staging. Without this picture, the pipeline will be decorative.

  1. Audit of the current build. We capture full build time, the set of modules, implicit dependencies on the local environment, and steps still performed manually.
  2. Pipeline design. We define stages: compilation, tests, static analysis, artifact assembly, publication, delivery to staging. Each has a trigger condition and failure criterion.
  3. Environment separation. Development, pre-production, and production receive their own configurations and access rights; the artifact is one, and parameters are injected at deployment.
  4. Secret storage. Keys, database passwords, and tokens move out of the repository and build files into secure storage with role-based access and an access log.
  5. Code quality checks. We connect static analysis, a coverage threshold, and dependency checks for known vulnerabilities: the build fails on violations, not after manual review.
  6. Training and handover. We show the team how to add stages and read logs, and hand over pipeline diagrams and maintenance policies.

This order produces an effect visible in numbers: the build runs reproducibly from the same commit, and developers begin extending the pipeline themselves, without contacting us for every change.

What we hand over: configurations, diagrams, policies

A pipeline that works but is not described anywhere lives only until the first engineer leaves: the build fails, and the cause is sought in chats and old tickets.

Therefore, along with setup, we hand over a set of documents by which your specialist can reproduce and change the build without us.

  • Pipeline description: stages, branch and tag triggers, artifact publication conditions, manual approval points.
  • Environment diagram: what is deployed in the test and production circuits, which branches are rolled out where, and where integration checks run.
  • Access rules: participant roles, who triggers a release, where secrets and keys are stored, how access is granted and revoked.
  • Build documentation: Maven or Gradle commands, dependency cache, environment requirements, and the procedure for reproducing the artifact locally.
  • Version release policy: numbering, pre-publication checklist, procedure for rolling back to the previous artifact, approval points.
  • Operations instructions: actions when a build or tests fail, where to look at logs, where notifications arrive.
  • List of external services: artifact registry, secret storage, build server, notifications — and who is responsible for each.

We hand over the set during a walkthrough: we demonstrate the pipeline on a live task and answer your engineers’ questions — after that, you can run the build on your own.

Case study: cut build time and rollbacks threefold

A multi-module application with several dozen modules was built entirely on every commit: a full run took about 40 minutes, tests ran in a single queue, and the dependency cache was not reused between tasks.

The developer learned about a build failure by the end of the hour, branches diverged, and the release went out twice a month and always with manual artifact rebuilding.

The cost of such a scheme is not only time. For every five releases there was one rollback, because an incompletely verified artifact went to production. Large changes accumulated locally to avoid breaking the main branch, and the release window turned into a crunch.

What we changed

We split the build into stages: the Maven cache is reused between tasks, modules are built in parallel according to the dependency graph, and only changed ones are rebuilt.

Checks were arranged by levels — unit tests run on every commit, integration tests and static analysis on merge to the main branch.

The artifact is built once and promoted across environments without rebuilding, so exactly what passed checks reaches production. Rollback is switching to the previous verified artifact, not a rerun.

The outcome

Full build dropped from 40 to 11 minutes, and time from commit to production dropped from several days to 45 minutes. The rollback rate fell from 18% to 6%, and releases instead of twice a month go out three times a week and run without manual operations.

How Maven differs from Gradle in build setup

The difference is not which build tool is “more correct”: Maven describes a build with a rigid lifecycle of phases and plugins bound to phases, while Gradle uses a set of programmable tasks with arbitrary dependencies between them.

Both produce the same build result; they diverge on repeat builds, non-standard logic, and maintenance.

The speed of a repeat build depends on what the tool considers changed. Gradle compares task inputs and outputs and rebuilds only affected modules, so in CI/CD a Java project with a dozen modules is not run entirely on every change.

In Maven, incrementality lives at the plugin level, and part of the work is redone across the entire project — in multi-module builds this is the main share of time.

Criterion Maven Gradle
Build description declarative XML, fixed phases scripted tasks, lazy configuration
Repeat build plugin-level incrementality input/output cache per module
Non-standard step only via a separate plugin any logic directly in the script
Entry threshold lower, configuration is predictable higher, discipline is needed in build code

Flexibility of task description is the second point of divergence.

In Apache Maven, code generation or conditional publication is arranged as a plugin, and any deviation from the lifecycle hits its boundaries; in Gradle, the same logic is written in the script and lives alongside application code — convenient as long as the team keeps it at the same quality.

Build migration is almost never instantaneous: we move modules in stages, verifying artifact composition and signatures, keep both builds in parallel until the result matches, and only then switch the release. Rollback to the original tool remains possible as long as the old configuration is not deleted.

Common technical concerns before starting work

Before starting, engineers and technical leads ask roughly the same set of questions: will the new pipeline fit the current stack, how will build migration go, will releases have to be stopped, and how to separate environments. Below are short answers; we verify each point on your repository before fixing the plan.

Can we move to the new pipeline without stopping releases?

Yes. We run the new pipeline in parallel with the current one and build the same artifact; we compare the result by checksums and dependency composition.

The old one remains operational until confirmation: switching is by branch or tag, and rollback is returning the previous reference. The release cycle is not interrupted.

Is the new pipeline compatible with our build?

For Maven and Gradle projects, we transfer the configuration as is: wrappers, multi-module trees, private artifact repositories.

If dependencies are pulled from a closed environment, we deploy build runners inside your network — nothing goes outside, and versions are pinned in the local cache.

How should a monorepo be built — entirely or by module?

By changed modules. The dependency graph is built once, then the build affects only the affected branches of the tree, and the cache is split by module so that a change in one service does not invalidate the rest. A full build runs on a schedule and before release.

How to separate environments and not drag configuration into the artifact?

The artifact is built once and promoted between environments without rebuilding — from test to production. Settings and secrets are injected at deployment from storage, and access to production values is restricted by roles. This removes a class of errors where a build with test parameters goes to release.

Let's discuss the task: what to send for an estimate and plan

The assessment will be concrete if we see the project, not a retelling. A volume of requirements is not needed — a snapshot of how the build and release work now is enough; we will gather the rest ourselves from the code and configurations. What is useful to have at hand:

  • Read access to the repository. It is important to see the branching: which branch is considered the release branch and how changes get into it.
  • Build structure: one module or several, where dependencies and build tasks are described, which artifacts are published and where.
  • Current release path: who and with what triggers the build, which steps are done manually and how long they take.
  • Tests: what is already covered, how long a full run takes, which checks are a mandatory barrier before rollout for you.
  • Environments: how many environments there are, how they differ, and how configurations and secrets get into them.
  • Bottlenecks: what breaks most often, how you roll back now and in what time.
  • Constraints and goal: where we place the build and artifact storage, what requirements exist for access and logs, and what should become possible — release by button, several rollouts per day, rollback without manual edits.

The team composition for such a task is a build and release engineer, an infrastructure engineer, and a lead who maintains contact with you. On your side, one person who knows the project is enough.

Describe the task in free form and attach what you have: we will come back with a work estimate, a stage plan, and the points where you make the decision.