Spring Boot 2.7 reached end of open source support in November 2023. If you’re still running it in mid-2026, you’re two and a half years into unpatched CVEs, and every new library version you want to use has already dropped Boot 2 compatibility. The migration to 3.x isn’t optional anymore. It’s overdue.
The good news: this migration is well understood by now. The tooling has matured, the gotchas are documented, and most of the mechanical work can be automated. I’ve done this upgrade on codebases ranging from small services to a 100K-line monolith, and the path is the same every time. Here it is.
The Migration at a Glance
Four changes account for almost all of the work:
- Java 17 baseline. Boot 3 requires Java 17 minimum.
- The jakarta namespace. Every
javax.*import from the EE world becomesjakarta.*. - Spring Security 6.
WebSecurityConfigurerAdapteris gone. Config must be rewritten. - Hibernate 6. Ships with Boot 3 and changes dialects, ID generation, and type handling.
Everything else, property renames, actuator endpoint changes, the Spring Cloud Sleuth to Micrometer Tracing move, is smaller and mostly mechanical.
Step 0: Get to Java 17 First
Do not combine the JDK upgrade and the Spring Boot upgrade in one change. Upgrade to Java 17 (or 21) while staying on Boot 2.7, deploy it, and let it soak in production. Boot 2.7 runs fine on Java 17, so this is a safe, independently verifiable step.
If you’re coming from Java 8, the JDK jump is its own project. I’ve written a full guide on that: Java 8 to 21 Migration Guide. The short version: watch for removed JavaEE modules (JAXB, JAX-WS), the module system’s reflection restrictions, and --add-opens flags your framework stack might need.
# Verify the app runs clean on 17 before touching Boot
./mvnw clean verify
java -version # should show 17+
Step 1: Get to the Latest 2.7.x and Kill Deprecation Warnings
Bump to the last 2.7.x release and fix every deprecation warning the compiler and startup logs give you. Spring Boot 2.7 deprecated the APIs that 3.0 removed, so warnings on 2.7 are your exact list of future compile errors.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.7.18</version>
</parent>
The most important one to handle now: rewrite your Spring Security configuration off WebSecurityConfigurerAdapter while still on 2.7. Spring Security 5.7 supports the new component-based style, so you can make this change and test it in isolation before the big bump. I cover the full rewrite in the Spring Security 5 to 6 migration guide.
Step 2: Run OpenRewrite
OpenRewrite automates the bulk of the mechanical migration: the jakarta namespace change, property renames, dependency updates, and many deprecated API replacements.
./mvnw org.openrewrite.maven:rewrite-maven-plugin:run \
-Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:RELEASE \
-Drewrite.activeRecipes=org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_5
Run it on a clean branch and review the diff carefully. In my experience it handles 60 to 80 percent of the changes correctly, and the diff review tells you exactly where the remaining manual work lives. I’ve written more about the workflow in Simplify Spring Boot Version Migration with OpenRewrite.
Step 3: The Jakarta Namespace Change
This is the change that touches the most files. Everything that lived under javax.* in the EE specs moved to jakarta.*:
// Before (Boot 2.x)
import javax.persistence.Entity;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
// After (Boot 3.x)
import jakarta.persistence.Entity;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotNull;
Note what does NOT change: javax.sql, javax.crypto, javax.naming, and other packages that belong to the JDK itself stay javax. A blind find-and-replace will break those. OpenRewrite gets this right; regex doesn’t.
The harder part is your dependency tree. Every library that touches servlet, persistence, or validation APIs needs a jakarta-compatible version. Check the usual suspects:
- Hibernate 6.x (comes with Boot 3)
- Tomcat/Jetty embedded versions (managed by Boot, but watch custom overrides)
- Swagger/OpenAPI: springfox is dead; move to
springdoc-openapi2.x - Any older SOAP, JAXB, or EJB client libraries: these are where migrations stall
I’ve covered the namespace migration in depth here: Jakarta EE Migration for Spring Boot 3.
Step 4: Bump the Boot Version and Fix Properties
Now change the parent version and target the latest 3.x line directly. Don’t step through 3.0, 3.1, 3.2. The breaking changes are concentrated at the 2.7 to 3.0 boundary, so intermediate stops add work without reducing risk.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.3</version>
</parent>
Add the properties migrator to catch renamed configuration keys at startup:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>
It logs every deprecated property it finds with the new name. Fix them all, then remove the dependency. Common renames you’ll hit:
# Before
spring.redis.host: localhost
management.metrics.export.prometheus.enabled: true
# After
spring.data.redis.host: localhost
management.prometheus.metrics.export.enabled: true
Step 5: Hibernate 6
Boot 3 ships Hibernate 6, and it’s a real major version with behavior changes, not just a dependency bump. The ones that bite:
- Dialect changes. Version-specific dialects like
MySQL57Dialectare gone; use the baseMySQLDialect(or better, remove the explicit dialect property and let Hibernate detect it). - ID generation. Sequence defaults changed; check that
@GeneratedValuemappings still match your database sequences before you find out via a unique constraint violation. - Timezone handling. Timestamp storage defaults changed, which matters if your servers and database run in different zones.
These deserve their own article, and they have one: Hibernate 5 to 6 Migration with Spring Boot 3.
Step 6: Observability Changes
If you use distributed tracing, Spring Cloud Sleuth is dead. Boot 3 moved tracing to Micrometer Tracing:
<!-- Before: Sleuth -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<!-- After: Micrometer Tracing with a Brave bridge -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
Actuator behavior changed too: httptrace became httpexchanges, and some endpoints moved or changed response formats. If you have dashboards or alerts built on actuator output, verify them against the new shapes. My Spring Boot monitoring guide covers the current setup.
Step 7: Regression Test Like You Mean It
The compiler catches the namespace change. It does not catch behavior changes in security rules, ID generation, timezone handling, or serialization. The migrations that go badly are the ones tested only by “it compiles and starts.”
Priorities, in order:
- Security rules. Every endpoint that should be protected, tested as protected. Spring Security rewrites are where silent auth regressions come from.
- Database integration tests. Testcontainers against your real database engine, not H2. Hibernate 6 differences often only appear on the real dialect.
- Serialization contracts. Date formats and null handling in API responses, especially if you upgraded Jackson along the way.
What Usually Goes Wrong
After several of these migrations, the failure patterns are consistent:
- A transitive dependency still pulls javax. Run
mvn dependency:treeand look for oldjavax.servletorjavax.persistencejars. Two servlet APIs on the classpath produce confusing runtime errors. - springfox. It never supported Boot 3 and never will. Migrating to springdoc is a mechanical but non-trivial change if you have lots of annotated controllers.
- Custom security filters. Anything that extended internal Spring Security classes tends to break. Rewrite against the public component model.
- Batch jobs. Spring Batch 5 (in Boot 3) changed
JobBuilderFactoryandStepBuilderFactoryto plain builders and made JobRepository transaction handling stricter.
When to Get Help
If your application is large, heavily customized in the security layer, or blocked on a dependency that has no jakarta-compatible version, the migration turns from mechanical work into an architecture exercise. That’s exactly the kind of work I do: I’ve taken production systems from Spring Boot 1.4 through 2.x to 3.5 on live, regulated products without stopping feature delivery. If you want an experienced hand on your migration, book a strategy call or read more about my Spring Boot consulting.
Once you’re on 3.5, the path to Spring Boot 4 is far gentler. Get through this one, and you’re back on the supported train.
Java Modernization Readiness Assessment
15 questions your team should answer before starting a migration. Takes 10 minutes. Could save you months.