← Blog
Code ModernizationDeveloper Skills

Migrating to Spring Boot 3.5 when you’re not ready for Spring Boot 4

Contents
  1. Why you should upgrade your services
  2. What you’ll need to update when migrating to Spring Boot 3.5
  3. Java 17 migration
  4. Java 21 and virtual threads
  5. Jakarta EE 10 migration
  6. Spring Boot migration
  7. How OpenRewrite auto-refactoring can help
  8. Getting to know the OpenRewrite recipe marketplace
  9. What’s left after the recipe runs
  10. Check out Moderne: migration engineering at scale
  11. Want to learn more?

While Spring Boot 4 is the current release line, there are a lot of teams that have dependencies preventing them from jumping to the latest version. For others, the jump from Spring Boot 2.x to 4 is just too big to take in one step. For those teams, Spring Boot 3.5 is the sweet spot. While OSS support ended in June 2026, Broadcom offers enterprise support for 3.5 through June 2032. Alternatively, Backpatch Alliance can help with ensuring access to critical backpatches for vulnerabilities discovered in EOL open source software. That makes Spring Boot 3.5 a stable base to land on while you plan the full move to Spring Boot 4.

Getting to Spring Boot 3.5 can still be a big migration though, especially coming from earlier Spring Boot 2.x versions. Spring Boot 3.5 brings Java 17, the move from javax to jakarta, Spring Framework 6 and several dependency upgrades. This guide covers what changes in the move to 3.5, how you can use OpenRewrite recipes to automate most of the migration, and what’s left to plan for.

Ready for Spring Boot 4? Check out our Spring Boot 4 migration guide.

Why you should upgrade your services

It’s easy to take an approach of “if it’s not broken, why fix it?” However, OSS support for Spring Boot 2.x ended in November of 2023, so 2.x applications no longer get free security fixes when new CVEs are published. Upgrading closes that gap and gives you access to a host of new features across many tools.

For example, by upgrading to Java 17 (or higher), which is a requirement for Spring Boot 3.0+, you’ll not only get a wide variety of new Java language features (records, pattern matching, switch expressions, etc.), but you’ll also benefit from performance improvements made to the virtual machine and garbage collector.

Note: Your environment will have a unique performance footprint. It is a good idea to use a monitoring platform to create a baseline of your applications’ performance today, so you can quantify the savings when you upgrade.

In regards to Spring Boot 3.x, you can benefit from better support for building native executables and using the GraalVM. By building your Spring applications as native executables, you’ll find significant improvements in startup time. You’ll also find that observability has been a key theme in this new version, with tracing now being implemented via Micrometer Tracing.

From Spring Boot 3.2 onwards you can take advantage of Virtual Threads in Java 21 and up. These allow you to serve more requests from the same hardware by offloading blocking IO operations without complicating the programming model.

In addition, Spring Boot 3.5 improves structured logging, adds SSL support for service connections, and allows you to load properties from multiline environment variables.

What you’ll need to update when migrating to Spring Boot 3.5

Moving to Spring Boot 3.5 includes a number of associated migrations and dependency updates that you must do prior to migrating to this new Spring Boot version, including:

  • Upgrade your organization’s applications, infrastructure, and CI/CD pipeline to use Java 17, 21, or 25. The good news with this step is that this work can be performed prior to upgrading any of your Spring Boot applications.

  • Any of your existing Spring applications that leverage Java EE will require an update to Jakarta EE 10. This may seem like a straightforward exercise that involves moving imports from the javax namespace to the jakarta namespace but this also requires that any third-party libraries also be migrated to versions that are compatible with Jakarta EE 10.

  • Finally, depending on which version of Spring Boot your applications are being migrated from, there may be several required changes to both the application’s code and configuration when moving to Spring Boot 3.5.

Java 17 migration

Spring Boot/Spring require a minimum baseline version of Java 17. This is the oldest LTS release Spring Boot 3 supports, so if you’re coming from Spring Boot 2.x you may need to update.

For those applications that are running on Java 8, the key sources of friction are the introduction of the module system in Java 9 and the removal of many of the J2EE javax dependencies from the core JDK. Applications that use any of the Java EE specifications will be required to add explicit dependencies to their projects. Additionally, there are many third-party dependencies that are not compatible with the Java module system that must be upgraded as part of this exercise.

Starting with Java 11 a new, six-month release cadence was adopted by the OpenJDK community and, with it, users must now deal with deprecated APIs. Deprecated features are initially non-fatal but are later removed from the Java platform. Applications that are upgrading across multiple versions of Java will have to address the API removed from the platform. Fortunately, there is a Java tool, Jdeprscan, that can be used to identify which APIs have been deprecated or removed from the platform. In addition, OpenRewrite has a recipe that enables you to look for all deprecated methods. Applications that are using these APIs will be required to remediate their solutions to successfully upgrade to Java 17.

Java 21 and virtual threads

To help with all of this, we’ve created Spring Boot 3.x best practices recipes (e.g., Spring Boot 3.5) that upgrade your app to the latest Spring Boot 3.x version, as well as upgrades to Java 21, with virtual threads. Going forward we’ll maintain this as a convenient recipe to get all the recommended recipes for Spring Boot applied at once.

As part of the upgrade to Java 21 we’ve also adopted methods from the new Sequenced Collections interface, as well as moved away from the now deprecated constructors for Locales and URLs. There are also a few other enhancements we’ve made, too. See the Migrate to Java 21 recipe for the full details.

Jakarta EE 10 migration

Spring Boot 3 moves from Java EE to Jakarta EE. If you’re moving from Spring Boot 2.x to 3.5, the biggest change comes from Jakarta EE 9, which renamed every enterprise package from javax.* to jakarta.*. Every import, annotation, deployment descriptor, and API dependency that uses those packages has to move to the new namespace at once. For teams going from 3.x to 3.5, the namespace changes are already handled and Jakarta EE 10 (which Spring Boot 3.5 runs on) keeps the new names and adds newer API versions such as Servlet 6.0.

Additionally, third-party libraries like Hibernate, Tomcat, and Jetty ship Jakarta-compatible versions. These will require users of Spring Boot to carefully review their third-party libraries to ensure that they have a compatible artifact. For instance, Spring Boot 3.5 expects Tomcat 10.1 or Jetty 12.

Fortunately, the Migrate to Jakarta EE 10 recipe can handle the namespace change, dependency upgrades, and deprecated API removals together.

Spring Boot migration

The 2.x line of Spring Boot was released in Feb 2018 and there have been seven minor version updates over that time. The Spring Boot team provides a migration guide for each version of Spring Boot where the most common issues are dealing with configuration changes, upgrading third-party dependencies and dealing with deprecated/removed APIs. Depending on which version of Spring Boot an application is currently running, the changes required to upgrade the application may be substantial.

Another factor that can be challenging for organizations is that an application may use multiple sub-projects from the Spring catalog (Spring Data, Spring Batch, Spring Integration, etc.) and each of these sub-projects has its own requirements when upgrading to newer versions.

How OpenRewrite auto-refactoring can help

The good news is that the OpenRewrite community, including open source and third-party vendors, are working on recipes for many of these complex changes. This means there will be significant portions of these updates that can be automated using OpenRewrite. For those who don’t know, OpenRewrite is a semantically-aware code search and transformation tool. It is capable of making sophisticated changes to code, build files, and configurations that are idiomatically consistent with the existing project’s formatting standards.

In fact, OpenRewrite has a large catalog of recipes that can help users as they migrate to Spring Boot 3.5 including:

  • Migrating from older versions of Java up to Java 25

  • Migrating from JUnit 4 to 5

  • Migrating from Java EE to Jakarta EE 10

  • Migrating from older versions of Spring Boot (1.5 -> 2.x, 2.7 ->3.0, 3.0 -> 3.x)

Recipes are composable, which enables larger-scale framework migrations to consist of calling a set of recipes that perform specific tasks or chaining other composite recipes together. For example, the recipe to migrate to Java 11 can take advantage of recipes that modify Maven dependencies and recipes that target the migration of specific, deprecated APIs. In turn the recipe to migrate to Java 17 can be chained to the Java 11 recipe, and the Spring Boot 3.x migration recipe can leverage those recipes along with a recipe that migrates the Jakarta EE libraries to version 10.

You can also use this composable nature of recipes to target specific components you’re not up-to-date with if you want to reduce the scope of the changes you’re making. For instance without going to Spring Boot 3.x, you could:

Let’s walk through some of the types of changes that can be automated with OpenRewrite:

OpenRewrite can intelligently make changes to existing Maven or Gradle build files to upgrade dependencies.

Coding changes are made safely because of OpenRewrite’s rich Lossless Semantic Tree (LST) model that allows new imports to be added (following the conventions of the existing source code) and code changes are indented properly.

And complex code changes can be made safely. In this example SimpleAuthConfig is modified to no longer extend WebSecurityConfigurerAdapter and the configure method is changed to return a new bean. The imports are adjusted, new imports are in the correct location, and formatting is consistent with the surrounding code.

Getting to know the OpenRewrite recipe marketplace

There are thousands of different recipes like the Spring Boot migrations currently available in the recipe marketplace, with more being added as the project continues to grow.

The OpenRewrite core framework is OSS and Apache licensed. This allows framework authors to provide free Apache-licensed migrations to their consumers (for example, Quarkus and Micronaut). However, organizations can also provide proprietary recipe packages and custom recipe authorship services to fill in the gaps. Moderne not only created OpenRewrite, but we also spend significant time developing recipes for the community and for our customers.

There are two distinct recipe modules available to aid your Spring Boot migrations:

  • org.openrewrite.recipe:rewrite-spring which is community maintained and source available under the Moderne Source Available License. Currently the community has provided recipes up to Spring Boot 4.0.

  • Moderne proprietary recipes available to users of Moderne, which cover the full Spring Boot 3.x and 4.x paths.

It’s also possible to create your own custom recipes and then apply those recipes to your projects to perform safe, automated refactoring that will save you and your team hours of work that would normally have to be applied by hand.

What’s left after the recipe runs

While OpenRewrite recipes can handle most of the Spring Boot 3.5 migration for you, they can’t handle everything for every codebase. When our own engineering team moved 37 repos (about 2,700 files) from Spring Boot 2 to 3 in late 2023, the recipe run took about a minute and the OpenRewrite recipes handled 80-90% of the work. But we still had a few things we had to do specific to our codebase. If you’re coming from Spring Boot 2.x, here’s a look at that work to help with your own migration planning.

  • Spring Framework 6 API changes. Every Spring Boot 3.x release runs on Spring Framework 6, so these apply to any 3.x version you target. Recipes can handle some of the HttpStatus to HttpStatusCode changes, but other call sites we had to fix manually, such as HttpStatus::isError in WebClient status handlers. HttpMethod also changed from an enum to a class, which affected how we serialized it between services.
  • Missing transitive dependencies. The newer libraries stopped pulling in some dependencies we were reliant on, such as io.grpc:grpc-netty. We declared them directly.
  • Observability. Spring Boot 3 moves metrics to the Micrometer Observation API. Our WebClient metrics used MetricsWebClientFilterFunction, so we replaced it with DefaultClientRequestObservationConvention.
  • Third-party wrappers. We needed to re-implement our Keycloak wrapper because the embedded wrapper didn’t have support for Spring Boot 3. As a plus, this also put us back on the current Keycloak release.
  • Additional library upgrades became unblocked. As a result of the Spring Boot migration, Netflix DGS framework could jump three major versions.

With just 37 repos, we were able to fix these by hand pretty quickly. But when you’re looking at thousands of repos, the custom recipes mentioned earlier become critical, and the time investment in building them pays for itself.

Going from Spring Boot 2.x to 3.x will still require a rollout plan. In our case, we pinned library and plugin versions so Spring Boot 2 services could still ship fixes mid-migration. We also kept the API gateway compatible with a mix of 2 and 3 services, then deployed the most-changed services first.

Check out Moderne: migration engineering at scale

If you’re working in an enterprise environment with hundreds or thousands of repositories, manually running these recipes on each repository isn’t feasible or scalable. You need multi-repository capabilities. This is where Moderne comes in. The Moderne Platform enables you to run each of these recipes across all of your repositories. Moderne also allows you to issue and track mass PRs across all your repositories.

You can see the detailed composite Spring Boot 3.5 migration recipe list below and watch the video to see how you can migrate in the Moderne Platform.

▶ Watch on YouTube

Moderne’s Recipe Builder (shown below) enables you to easily visualize, edit, and sequence recipes for the specific needs of your organization. This is especially useful with large scale migrations like Spring Boot 3.5 that has thousands of steps.

You can also work in the multi-repo Moderne CLI to accelerate your migration.

Want to learn more?

Watch the Code Remix Weekly session to learn more about the Spring Boot 3.5 migration.

▶ Watch on YouTube

You can join the OpenRewrite Slack channel to ask questions and interact with the community that is developing all of these recipes. Or, check out the documentation which includes more information on how to get started and what OpenRewrite can do.

Also, contact Moderne to learn more about how you can use our platform with your own codebase.

Written by Tim te Beek, Mike Solomon, and Peter Streef

Originally published