Site icon JVM Advent

Out with the Old, In with the New: A Guide to Application Upkeep

Migrating an existing application to a new version of Java or a framework such as Spring Boot involves much more than simply updating a version number in a file. Each new release of a library or language brings new features, deprecations, behavioral changes, and sometimes complete API redesigns. When legacy code is involved, these upgrades may require revisiting old implementations and correcting patterns that no longer work. Even the slightest change can trigger a cascading effect, modifying other parts of the application in unexpected ways.

These migration challenges require significant effort even for a single application. The difficulty grows exponentially when a company needs to upgrade dozens of systems at once. And once the migration is complete, a new question arises: how can we keep applications consistently up to date without repeating the same painful process?

CONTEXT OF THE SITUATION

Imagine a team responsible for several microservices that use different versions of Java or Spring Boot. This happens because not all microservices change frequently; at some point, a problem occurs with a library they all use, and when someone tries to update the version, another problem arises, such as the new version not supporting all versions of Java.

To see this situation graphically, check the following table, which represents a possible scenario that could occur in any company:

API Compilation Code Style Framework
api-a 8 8 Spring Boot 1.5.7
api-b 11 8 Spring Boot 2.1.4
api-c 17 17 Spring Boot 3.0.0
api-d 14 11 Spring Boot 2.3.3

With this problem in mind, the only alternative is to create a custom solution for each application that does not use the minimum version of Java that supports this library, but this could have many implications, such as performance issues and more time to fix the problem across all the microservices.

The Problems with Legacy Code

Legacy code does not always need to be updated. For example, a desktop application that operates in isolation and does not interact with external services may continue to function without requiring significant changes. In these scenarios, updates are optional.

However, in many other cases, applications must be updated for several reasons, mainly when they rely on outdated Java versions or obsolete dependencies. Running outdated software introduces risks and limitations that can directly affect stability, security, and maintainability.

Some of the risks associated with outdated code are:

HOW TO SOLVE THESE PROBLEMS?

The problem could be split into two situations: one is an application that contains legacy code and needs to be updated, and the other is an application that uses a relatively recent version of a language or framework but has some dependencies on older versions.

There is a set of tools or libraries to solve these problems, the most popular are:

Let’s see in the following table a brief comparison of all of them:

Main differences Reno vate

Open Rewrite

Maven plugin

Dependa Bot

 It has good documentation.
Support multiple languages.

Refactoring the source code to a specific version.

Automates dependency updates.

It’s possible to generate pull/merge requests.

It has a large user community.

In this comparison, the best option for addressing migration problems is OpenRewrite. If you need to keep dependencies up to date, the winners could be Dependabot or Renovate. However, given the cost of these tools, the winner is the Maven Plugin.

This article uses a source from a GitHub repository; feel free to clone it and use it to learn about how to keep the application updated.

Solution #1: Leveraging OpenRewrite

OpenRewrite provides a comprehensive solution to code migration challenges with automated, reliable refactoring tools that streamline the process. The platform effectively highlights necessary code changes, implements consistent transformations, and significantly minimizes the manual effort required for application updates. By automating repetitive tasks and guiding developers through essential modifications, OpenRewrite improves the manageability of large-scale upgrades while reducing the risk of errors.

The migration process with OpenRewrite follows a precise, structured flow, offering a series of recipes that include the steps and resources needed at each stage. This tool is not limited to migrating Spring Boot or Java; it offers a comprehensive catalog of recipes for different languages and frameworks.

Let’s see how the process of migrating an application to Java 21 and Spring Boot 3.4 is. To do that, add the OpenRewrite plugin or dependency to your build system, along with the recipes to be used, as shown in the following code block.

<plugin>

<groupId>org.openrewrite.maven</groupId>

<artifactId>rewrite-maven-plugin</artifactId>

<version>6.24.0</version>

<configuration>

<activeRecipes>

<recipe>org.openrewrite.java.OrderImports</recipe>

<recipe>org.openrewrite.java.migrate.UpgradeToJava21</recipe>

<recipe>org.openrewrite.java.spring.boot3.SpringBootProperties_3_4</recipe>

</activeRecipes>

</configuration>

<dependencies>

<dependency>

<groupId>org.openrewrite.recipe</groupId>

<artifactId>rewrite-spring</artifactId>

<version>6.19.0</version>

</dependency>

<dependency>

<groupId>org.openrewrite.recipe</groupId>

<artifactId>rewrite-migrate-java</artifactId>

<version>3.9.0</version>

</dependency>

</dependencies>

</plugin>

As a recommendation, check the latest version of this plugin on the official webpage or a repository like this regularly.

The plugin only contains the core logic to execute the receipts that are necessary to include as external dependencies, as shown in the previous code block. Also, it’s possible to create custom receipts that are not part of the official library.

The next step after the modifications on the project is to execute the changes using the following command:

$ mvn rewrite:run

[INFO] Using active recipe(s) [org.openrewrite.java.OrderImports, org.openrewrite.java.migrate.UpgradeToJava21, org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_4]
[INFO] Using active styles(s) []
[INFO] Validating active recipes...
[INFO] Project [api-reservations] Resolving Poms...
[INFO] Project [api-reservations] Parsing source files
[WARNING] locking FileBasedConfig[/home/asacco/.config/jgit/config] failed after 5 retries
[INFO] Running recipe(s)...
[WARNING] Changes have been made to api-reservations/pom.xml by:
[WARNING] org.openrewrite.java.migrate.UpgradeToJava21
[WARNING] org.openrewrite.java.migrate.UpgradeBuildToJava21
[WARNING] org.openrewrite.java.migrate.UpgradeJavaVersion: {version=21}
[WARNING] org.openrewrite.maven.UpdateMavenProjectPropertyJavaVersion: {version=21}
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_4
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springframework.boot, artifactId=*, newVersion=3.1.x, overrideManagedVersion=false}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springdoc, artifactId=*, newVersion=2.2.x}
[WARNING] org.openrewrite.java.testing.mockito.Mockito4to5Only
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.mockito, artifactId=*, newVersion=5.x}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springframework.boot, artifactId=*, newVersion=3.2.x, overrideManagedVersion=false}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springdoc, artifactId=*, newVersion=2.5.x}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springframework.boot, artifactId=*, newVersion=3.3.x, overrideManagedVersion=false}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springdoc, artifactId=*, newVersion=2.6.x}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springframework.boot, artifactId=*, newVersion=3.4.x, overrideManagedVersion=false}
[WARNING] org.openrewrite.java.dependencies.UpgradeDependencyVersion: {groupId=org.springdoc, artifactId=*, newVersion=2.8.x}
[WARNING] Changes have been made to api-reservations/src/main/java/com/twa/reservations/connector/CatalogConnector.java by:
.....

If everything works as expected and the migration was successful, the changes to the POM file that add the openRewrite plugin will be removed.

Suppose it’s necessary to understand and see all the changes that could affect the application before doing so. In that case, another command shows that information by simulating execution and displaying the results.

$ mvn rewrite:dryRun
....
[INFO] Using active recipe(s) [org.openrewrite.java.OrderImports, org.openrewrite.java.migrate.UpgradeToJava21, org.openrewrite.java.spring.boot3.SpringBootProperties_3_4]
[INFO] Using active styles(s) []
[INFO] Validating active recipes...
[INFO] Project [api-reservations] Resolving Poms...
[INFO] Project [api-reservations] Parsing source files
....
[WARNING] These recipes would make changes to api-reservations/src/main/java/com/twa/reservations/controller/ReservationController.java:
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_4
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_3
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_1
[WARNING] org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_0
[WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_7
[WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_6
[WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_5
[WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_4
[WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_3
[WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_2
[WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_1
[WARNING] org.openrewrite.java.spring.boot2.UpgradeSpringBoot_2_0
[WARNING] org.openrewrite.java.spring.boot2.SpringBoot2BestPractices
[WARNING] org.openrewrite.java.spring.NoAutowiredOnConstructor
[WARNING] Patch file available:
[WARNING] /home/asacco/Code/Talks/out-with-the-old/api-reservations/target/rewrite/rewrite.patch
[WARNING] Estimate time saved: 20m
[WARNING] Run 'mvn rewrite:run' to apply the recipes.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 20.671 s
[INFO] Finished at: 2025-12-01T15:22:34-03:00
[INFO] ------------------------------------------------------------------------

On the file rewrite.patch that was created in the target folder, all the changes that will be executed in the application will appear.

Solution #2: Dependency – Updates Versions

The versions-maven-plugin is a powerful Maven tool designed to help developers keep project dependencies, plugins, and parent versions up to date. With simple commands, it can identify outdated components, suggest the latest compatible versions, and even update itself pom.xml automatically. This reduces the manual effort required to track version changes and helps ensure applications remain secure, stable, and aligned with the latest improvements in their ecosystems.

The first step to use this plugin is to include it on the pom file like appears on the following block:

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.18.0</version>
</plugin>

As a recommendation, check the latest version of this plugin on the official webpage or a repository like this regularly.

The next and last step is to run a command that checks all dependencies and the project and suggests which are outdated. The command and the output looks like the following:

$ mvn versions:display-dependency-updates
...
[INFO] --- versions:2.18.0:display-dependency-updates (default-cli) @ api-reservations ---
[INFO] The following dependencies in Dependency Management have newer versions:
[INFO] biz.aQute.bnd:biz.aQute.bnd.annotation ................ 7.0.0 -> 7.1.0
[INFO] co.elastic.clients:elasticsearch-java ................ 8.15.5 -> 9.2.1
[INFO] com.couchbase.client:java-client ..................... 3.7.9 -> 3.10.0
[INFO] com.datastax.oss:native-protocol ...................... 1.5.1 -> 1.5.2
[INFO] com.fasterxml.jackson.core:jackson-annotations ..... 2.18.5 -> 3.0-rc5
[INFO] com.fasterxml.jackson.core:jackson-core ............. 2.18.5 -> 2.20.1
[INFO] com.fasterxml.jackson.core:jackson-databind ......... 2.18.5 -> 2.20.1
[INFO] com.fasterxml.jackson.dataformat:jackson-dataformat-avro ...
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.970 s
[INFO] Finished at: 2025-12-01T16:43:32-03:00
[INFO] ------------------------------------------------------------------------

Consider that with this command, all the dependencies related to Spring Boot will appear; for example, they could be updated by simply incrementing the framework’s version. This scenario is a good candidate to use another approach which only show the version of the dependencies that are declared on the pom file. The command is quite similar to the previous one, but the output is entirely different, as shown in the following block.

$ mvn versions:display-property-updates
...
[INFO] The following version properties are referencing the newest available version:
[INFO] ${maven-failsafe-plugin.version} .............................. 3.5.4
[INFO] ${mockito.version} ........................................... 5.20.0
[INFO] The following version property updates are available:
[INFO] ${datafaker.version} ................................. 2.3.0 -> 2.5.3
[INFO] ${formatter-maven-plugin.version} .................. 2.23.0 -> 2.29.0
[INFO] ${instancio-junit.version} ........................... 5.2.1 -> 5.5.1
[INFO] ${junit-platform-launcher.version} ................ 1.8.2 -> 6.1.0-M1
[INFO] ${junit.version} ................................. 5.10.1 -> 6.1.0-M1
[INFO] ${mapstruct.version} ........................... 1.5.5.Final -> 1.6.3
[INFO] ${maven-compiler-plugin.version} ............. 3.14.1 -> 4.0.0-beta-3
[INFO] ${maven-enforcer-plugin.version} ..................... 3.4.1 -> 3.6.2
[INFO] ${maven-surefire-plugin.version} ..................... 3.1.2 -> 3.5.4
[INFO] ${spring-boot-starter.version} ...................... 3.4.12 -> 4.0.0
[INFO] ${springdoc-openapi-starter-webmvc-ui.version} ...... 2.8.14 -> 3.0.0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.970 s
[INFO] Finished at: 2025-12-01T16:43:32-03:00
[INFO] ------------------------------------------------------------------------

This plugin offers a series of other commands to check specific parts of the POM file, such as plugins, and many others, so it’s recommended to use the appropriate one depending on the context.

WHAT’S NEXT?

There are many resources for the evolution of a platform or application. The following is just a short list of resources:

Other resources could be great for understanding some concepts related to the evolution of a platform in depth:

Consider this just a small list of available resources. If something is unclear, find another video or resource.

CONCLUSION

There is no silver bullet for keeping an application permanently up to date, but combining the proper practices with the right tools can dramatically simplify the process. Tools such as OpenRewrite or the Maven Versions Plugin automate much of the Java and Spring Boot migration process. Still, they cannot fix everything, especially when a library does not support newer Java versions. For example, Orika, a widely used mapping library, does not support Java 17 or later, so developers must manually migrate to an alternative before they can benefit from automation.

Because of these limitations, maintaining a clean codebase, adopting a strong testing culture, and updating dependencies regularly are essential. By combining these practices with the tools discussed in this article, development teams can reduce migration risks and ensure that future upgrades become far less painful.

Author: Andres Sacco

Andres Sacco has been a developer since 2007 in different languages, including Java, PHP, NodeJs, Scala, and Kotlin. His background is mostly in Java and the libraries or frameworks associated with this language. In most of the companies he worked for, he researched new technologies to improve the performance, stability, and quality of the applications of each company. In 2017 he started to find new ways to optimize the transference of data between applications to reduce the cost of infrastructure. He suggested some actions, some of them applicable in all the manual microservices and others in just a few. All this work concludes with the creation of a series of theoric-practical projects, which are available on the page Manning.com Recently he published a book on Apress about the last version of Scala. Also, he published a set of theoric-practical projects about uncommon ways of testing, like architecture tests and chaos engineering. He dictated internal courses to different audiences like developers, business analysts, and commercial people. Also, he participates as a Technical Reviewer on the books of the editorials: Manning, Apress, and Packt.
Exit mobile version