Migrating to Spring Boot 3.x: A Practical Guide for Legacy Projects
No more fear: a 5-step checklist for upgrading old Spring applications, without the surprises.
Introduction: Why the Jump From 2.x to 3.x/4.x Feels Intimidating
If your team is still running Spring Boot 2.x in 2026, you are far from alone, and you are also further behind than you might think. Spring Boot 3.0 shipped back in November 2022, Spring Boot 4.0 arrived in November 2025 on top of an entirely new Spring Framework 7, and 4.1 followed in June 2026. That means a straight 2.x-to-3.x jump today is really a decision about which of two moving targets to aim for, and that ambiguity is a big part of why the migration feels bigger than it is.
The good news is that the actual technical work is well understood, mechanical in most places, and heavily tooled. The fear usually comes from not knowing the shape of the job in advance. This guide lays that shape out plainly, in five steps, using nothing more exotic than your existing build tool and a couple of well-established automation tools.
The Biggest Hurdles: Java 17+ and the Jakarta EE Namespace Change
Nearly everything else in this migration is a consequence of two decisions the Spring team made deliberately. Get comfortable with both before touching any code.
First, Spring Framework 6 and Spring Boot 3.0 raised the minimum runtime to Java 17. There is no supported path that keeps you on Java 8 or 11 and moves to Boot 3. If your application, or any of its transitive dependencies, still assumes an older JDK, that gets resolved before anything else.
Second, and this is the change that touches the most files, Jakarta EE 9 renamed every enterprise Java package from the javax.* namespace to jakarta.*. This wasn’t Spring’s decision. Oracle transferred the Java EE specifications to the Eclipse Foundation but kept the trademark on the word “javax,” so the Eclipse Foundation had no legal option except to rename the packages it now maintains. Spring inherited that rename, and so does every application built on Servlet, JPA, Bean Validation, or CDI, whether or not that application ever touches those specifications directly.
Free Security Support Remaining, by Spring Boot Line (as of August 2026)

Spring Boot 2.7 stopped receiving free OSS security patches in November 2023. If that’s still your production line today, you have been running without upstream security fixes for roughly two and a half years, patched only by whatever your team backports manually or pays a third party like Tanzu or HeroDevs to backport for you. That gap, not any particular feature, is the real argument for moving now rather than later.
Step 1: Preparing Your Build
Resist the urge to jump straight to 3.x or 4.x from an old 2.x minor. The Spring team’s own advice is to land on the latest 2.7.x patch first, because 2.7 ships deprecation warnings for exactly the APIs that get removed in 3.0. Those warnings are, in effect, a free to-do list generated by the framework itself.
Once you’re on the latest 2.7.x, upgrade the JDK to 17 (or later) and get your build compiling cleanly under it before changing a single Spring dependency. Separating the JDK upgrade from the framework upgrade means that when something breaks, you know immediately which change caused it.
With Java 17 in place, bump the Spring Boot version itself. For Maven projects, that’s a one-line change to the parent version:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
</parent>
That single line will not make your project compile on its own, but it does immediately surface, via build errors, every dependency that isn’t yet compatible with the new baseline, which is exactly the information you want at this stage. Check third-party starters (database drivers, cloud SDKs, observability agents) against their own release notes; most major libraries published Boot-3-compatible versions well before 3.0 shipped, but plugins for build tools like the Maven Shade plugin or Gradle’s Spring Boot plugin often need an explicit version bump too.
Step 2: The Namespace Migration: Handling the javax to jakarta Transition
This step has a genuinely dangerous shortcut that circulates constantly, so it’s worth naming directly: do not blindly find-and-replace every occurrence of javax with jakarta across your codebase.
find . -name '*.java' -exec sed -i 's/javax/jakarta/g' {} +
That command looks like it solves the whole problem, and for a toy project with one controller and one entity, it might even compile. The trap is that not every javax package moved. Packages that belong to the JDK itself, such as javax.sql, javax.crypto, and javax.swing, were never part of Jakarta EE and were never renamed. A blind global replace corrupts every one of those imports into a namespace that doesn’t exist, and depending on how many of them your codebase has, that can be a worse mess than the migration itself.
The safer path is to let a tool that actually understands the distinction do the rewriting. The Eclipse Foundation’s own Jakarta EE Transformer project was built specifically for this, and Spring-specific OpenRewrite recipes can apply the same logic directly to your source tree as part of a build step, correctly limiting the rename to the packages Jakarta EE actually owns. Run either tool, then compile, then read the remaining errors: what’s left after an automated pass is almost always a genuinely manual decision, like a custom class that implemented a now-renamed interface, not a mechanical rename that a script should have caught.
Step 3: Dealing With Deprecated Libraries
Once the build compiles, the remaining failures tend to cluster around a small number of well-known trouble spots rather than being spread evenly across the codebase.
- Spring Security 6. The configuration model changed substantially; the old
WebSecurityConfigurerAdapterclass is gone, replaced by a component-based, bean-driven approach to defining security filter chains. Security configuration is consistently the single largest source of manual rewriting in a Boot 3 migration, more than the namespace change itself. - HTTP clients. If you built custom REST clients on Apache HttpClient 4.x, plan to move to HttpClient 5.x, whose API renamed and restructured several core classes.
- Actuator endpoint changes. Some management endpoint IDs and default exposure settings changed between 2.x and 3.x; anything scraping Actuator directly, rather than through Micrometer, is worth re-checking.
- Anything still on javax annotations outside Jakarta EE proper, such as older validation or JAXB libraries that haven’t published a jakarta-namespaced release. These sometimes require a version bump to a library you didn’t realize was still on the old namespace internally.
Treat your 2.7.x deprecation warnings from Step 1 as the master checklist here. Nearly every “why did this break” surprise during a 3.x migration turns out to be an API that was already flagged as deprecated for at least one full minor version beforehand.
Step 4: Testing Your Migration Strategy
A framework-level migration is exactly the wrong place to also be testing your application’s business logic for the first time. If you don’t already have a reasonably complete test suite, the honest, if unwelcome, first move is to write characterization tests against the current 2.x behavior before you touch anything else. Those tests become your regression net for the entire migration.
Once the suite exists, migrate one minor version at a time rather than leaping straight from, say, 2.7 to 3.4. Run the full suite after each step. This is slower than a single big-bang jump, but it turns “something broke somewhere in this enormous diff” into “something broke in this one specific minor version bump,” which is a dramatically easier problem to debug. Pay particular attention to integration tests that exercise the servlet container, JPA entity mappings, and security filter chains directly, since those are precisely the three areas Steps 2 and 3 touch the most.
Step 5: Choosing Your Landing Version and Rolling Out Safely
Here’s the decision the original 2.x-to-3.x plan usually skips, and it matters more now than it did in 2023. Spring Boot 3.5 was the final 3.x minor, which under Spring’s own support policy earns an extended enterprise support window, but its free OSS patches stopped in June 2026. Spring Boot 4.0 and 4.1, built on the new Spring Framework 7, are the actively supported line today, still on a Java 17 minimum, with support extending up through Java 26.
If your migration is starting now, landing on 3.5 as a stepping stone and then moving to 4.x shortly after is a reasonable, lower-risk sequence, since 3.5 is the closest target to your current 2.x code and 4.x’s changes on top of 3.x are comparatively smaller than the 2.x-to-3.x jump you’ve already absorbed. Whichever version you land on, roll it out the same way you’d roll out any major dependency change: behind a canary or a small percentage of production traffic first, with the previous version’s artifact kept ready to redeploy immediately, and monitoring specifically on error rates and P99 latency for the first 24 to 48 hours, since regressions from a framework migration tend to surface under real production load, not in a staging smoke test.
Boot 2.x vs. 3.x vs. 4.x, at a Glance
| Requirement | Spring Boot 2.x | Spring Boot 3.x | Spring Boot 4.x |
|---|---|---|---|
| Minimum Java version | Java 8 | Java 17 | Java 17 (tested through Java 26) |
| Enterprise namespace | javax.* | jakarta.* (EE 9/10) | jakarta.* (EE 10/11) |
| Underlying framework | Spring Framework 5.x | Spring Framework 6.x | Spring Framework 7.x |
| OSS security support (as of Aug 2026) | Ended Nov 2023 | Ended Jun 2026 | Active |
| Native image support | Not supported | Supported via GraalVM | Supported via GraalVM |
The 5-Step Checklist, Recap
- Step 1: Land on the latest 2.7.x, upgrade to Java 17, then bump the Boot version to surface incompatible dependencies immediately.
- Step 2: Migrate javax to jakarta with a tool that understands the JDK/Jakarta EE distinction, never a blind sed replace.
- Step 3: Work through Spring Security 6, HTTP client 5.x, and Actuator changes using your 2.7.x deprecation warnings as the checklist.
- Step 4: Build a regression test suite first if you don’t have one, then migrate one minor version at a time, testing after each.
- Step 5: Decide deliberately between landing on 3.5 or jumping straight to 4.x, then roll out behind a canary with a fast rollback path ready.
Conclusion: The Long-Term Gains Are Worth the Effort
None of the five steps above are individually difficult; the migration feels intimidating mainly because the namespace change, the Java baseline jump, and several library-level breaking changes all land in the same release. Broken into a build upgrade, a tooled namespace migration, a focused pass through the handful of libraries that actually changed their API surface, and an incremental, well-tested rollout, the work becomes tractable for almost any team.
What you get on the other side is a codebase back on a security-patched, actively maintained line, with a real path to Java 21 or newer language features, and, if you eventually need it, the option to build GraalVM native images for faster cold starts. Staying on Spring Boot 2.x doesn’t freeze your risk in place, it quietly increases it every month, since the gap between your framework’s last patch and today’s threat landscape only grows. The five steps here are the fastest honest way to close that gap.




