Spring Boot 2 to 3 Migration: Breaking Changes & Upgrade Guide for Java Devs
Complete guide to migrating from Spring Boot 2 to 3, covering Jakarta EE 9+, Java 17 baseline, Hibernate 6, and real production pitfalls.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Java 17 or later installed on your development machine
- ✓A Spring Boot 2.7.x application with a comprehensive test suite
- ✓Familiarity with Maven or Gradle build tools and dependency management
- ✓Access to a staging environment that mirrors production for validation
โข Spring Boot 3 requires Java 17+ and Jakarta EE 9 (javax โ jakarta package rename) โข Hibernate 6 replaces Hibernate 5 with breaking JPA changes and new SQL dialect handling โข Spring Security 6 removes deprecated WebSecurityConfigurerAdapter and uses lambda DSL โข Key dependencies: upgrade Gradle/Maven plugins, verify third-party library compatibility โข Common fail points: EhCache 3, Flyway 9, Lombok 1.18.24+, and custom auto-configuration
Think of Spring Boot 2 as a 2019 iPhone with iOS 13, and Spring Boot 3 as a 2023 iPhone with iOS 17. Your old apps used 'javax' like an old charging port โ Spring Boot 3 switched to 'jakarta' (USB-C). All the internal wiring changed: the CPU (Java 17), the OS (Jakarta EE), and the engine (Hibernate 6). You can't just drop in the new OS; you need to update every component that talks to it.
If you're reading this, you probably have a Spring Boot 2.7.x application in production and are staring down the barrel of the Spring Boot 3 migration. I've been there โ three times in the past year with different clients. The official migration guide from Pivotal/VMware is decent, but it glosses over the real pain points that will wake you up at 3 AM.
Spring Boot 3.0.0 was released in November 2022, and as of early 2025, 3.2.x is the stable line. The jump from 2.x to 3.x is the most disruptive since Boot 1 to 2. Why? Three tectonic shifts: Java 17 baseline (no more Java 8 or 11), Jakarta EE 9 (the javax to jakarta rename), and Hibernate 6 (which broke JPA queries and mappings).
This guide is not a rehash of the Spring Initializr output. It's a war story collection from actual migrations I've led on payment-processing systems handling $2M/day, SaaS billing platforms, and real-time analytics pipelines. I'll show you the code changes, the hidden traps, and the production incident that taught me to never trust a minor version upgrade without full integration tests.
We'll cover the breaking changes in Spring Framework 6, Spring Security 6, Hibernate 6, and the dependency ecosystem. You'll get concrete code examples for the Jakarta migration, the new SecurityFilterChain DSL, and Hibernate 6's implicit naming strategy. We'll also talk about what the official docs won't tell you: the third-party library hell, the test framework incompatibilities, and the monitoring blind spots.
By the end, you'll have a checklist to migrate your app safely, a debugging cheat sheet for common failures, and the confidence to plan the upgrade without losing sleep.
1. The Jakarta EE 9 Package Rename: From javax to jakarta
This is the single most disruptive change in Spring Boot 3. The entire Java EE API was transferred from Oracle to the Eclipse Foundation and rebranded as Jakarta EE. Every package that started with javax. โ javax.persistence, javax.servlet, javax.validation, javax.transaction โ was renamed to jakarta.. This is not a simple search-and-replace; it affects your source code, your build files, your IDE configuration, and every library you depend on.
If you use IntelliJ IDEA, the built-in migration tool (Code > Migrate to Jakarta EE 9) handles about 80% of the rename. But it misses things like string literals in configuration files, XML namespace declarations, and reflection-based code. For example, if you have a Hibernate entity that references javax.persistence in a native query, the tool won't touch it.
Here's the concrete change in a Spring Data JPA repository:
2. What the Official Docs Won't Tell You
The official Spring Boot 3 migration guide is a well-written document, but it assumes your application is a greenfield project or has perfect test coverage. In reality, most of us maintain 5+ year old monoliths with 30% test coverage and a web of transitive dependencies. Here's what the docs don't emphasize enough:
First, the javax to jakarta rename is not just a source code change. It breaks your build tool's dependency resolution. Maven's dependency:tree and Gradle's dependencies task will show you what you asked for, but not what you actually get at runtime. We found a case where a library declared a compile-time dependency on javax.servlet-api 4.0.1, but at runtime it pulled in jakarta.servlet-api 6.0.0 from Spring Boot's BOM. The result? NoClassDefFoundError at startup.
Second, the docs say 'upgrade to Hibernate 6' but don't tell you that Hibernate 6 changes the default naming strategy. In Hibernate 5, the default PhysicalNamingStrategy was SpringPhysicalNamingStrategy which converted camelCase to snake_case. In Hibernate 6, the default changed to CamelCaseToUnderscoresNamingStrategy, which behaves slightly differently for embedded fields and composite keys. We had a join column that mapped to user_address_id in Hibernate 5 but became userAddressId in Hibernate 6, causing a schema mismatch.
Third, the official migration guide recommends upgrading to Spring Boot 2.7.x first. That's good advice, but they don't tell you that 2.7.x deprecates a lot of APIs that you might rely on, like the WebSecurityConfigurerAdapter in Spring Security. You'll get compile warnings, but they won't break your build. Ignore them at your own risk โ they become hard errors in 3.0.
Finally, the docs assume you're using the default dependency management. If you have custom BOMs or dependency overrides, you're on your own. We had a client who pinned Hibernate 5.6.x in their parent POM, overriding Spring Boot 3's Hibernate 6. The application compiled but failed at runtime with Hibernate 5's unsupported dialect for PostgreSQL 15.
3. Spring Security 6: The Death of WebSecurityConfigurerAdapter
Spring Security 6, which ships with Spring Boot 3, finally removes the deprecated WebSecurityConfigurerAdapter class. If you've been using the 'extend and override' pattern since Spring Security 3, this is a hard break. The new approach uses a component-based security configuration with SecurityFilterChain beans and lambda DSL.
This is actually a good thing. The old adapter pattern encouraged a monolithic security configuration where everything was in one class. The new lambda DSL is more explicit, testable, and composable. But the migration requires rewriting every security configuration class.
Here's the typical migration:
4. Hibernate 6: JPA Queries and Naming Strategy Changes
Hibernate 6 is a major rewrite under the hood. The most visible changes for developers are in the query engine and the default naming strategy. If you use JPQL, Criteria API, or native queries, you need to test every single one.
First, the Hibernate 6 query parser is more strict about implicit joins. In Hibernate 5, you could write FROM User u WHERE u.address.city = 'Paris' and it would implicitly join the Address table. In Hibernate 6, this still works, but the generated SQL is different and can cause performance regressions if you relied on the old join order. We saw a query that joined 5 tables in Hibernate 5 but 8 tables in Hibernate 6 because it added extra joins for lazy-loaded collections.
Second, the default naming strategy changed. In Hibernate 5 with Spring Boot, the default was SpringPhysicalNamingStrategy which converted camelCase property names to snake_case column names. In Hibernate 6, the default is CamelCaseToUnderscoresNamingStrategy. The difference is subtle but critical: the new strategy doesn't handle embedded fields the same way. For example, an embedded Address with a field streetName would map to address_street_name in Hibernate 5 but to addressStreetName in Hibernate 6.
Here's how to restore the old behavior:
5. Dependency and Build Tool Changes: Maven/Gradle Plugin Updates
Spring Boot 3 requires updated build plugins and may break your CI/CD pipeline if you're using older versions. Here are the minimum versions you need:
- Maven: 3.6.3+ (3.8.x recommended)
- Gradle: 7.5+ (8.x recommended for optimal performance)
- Maven compiler plugin: 3.10.1+ (supports Java 17)
- Maven surefire plugin: 3.0.0-M7+ (for JUnit 5 compatibility)
If you use Gradle, the Spring Boot plugin changed its configuration. The bootRun task now requires Java 17, and the bootJar task uses a different classpath layout. We had a case where the bootJar task failed because it couldn't find a class that was in a custom dependency configuration.
For Maven users, the spring-boot-maven-plugin version is managed by the parent POM, but if you have custom plugin configurations, you need to verify they're compatible with Spring Boot 3. The repackage goal changed its default layout from 'ZIP' to 'JAR', which can break custom classloaders.
Here's the Maven plugin configuration for a smooth migration:
6. Third-Party Library Compatibility: EhCache, Flyway, Lombok, and More
This is where most migrations stall. Spring Boot 3's Jakarta EE 9 base means every library that touches javax.* must be updated. Here's the compatibility status of common libraries as of early 2025:
- Lombok: 1.18.24+ supports Java 17 and Jakarta EE 9. Older versions cause compilation errors with @Data and @Builder on entities.
- MapStruct: 1.5.3+ supports Jakarta. Older versions generate code with javax imports.
- Flyway: 9.0+ supports Jakarta. Flyway 8.x will fail at runtime with ClassNotFoundException for javax.sql.DataSource.
- EhCache: 3.10.0+ supports Jakarta. EhCache 2.x is deprecated and incompatible.
- Hibernate Validator: 8.0.0+ (the Jakarta version). Spring Boot 3 manages this, but if you override it, use the jakarta variant.
- Thymeleaf: 3.1.0+ supports Jakarta. Thymeleaf 3.0.x uses javax.servlet.
- Spring Cloud: 2022.0.0+ (codename Kilburn) is compatible with Spring Boot 3. Earlier versions are not.
- Spring Data: All modules are updated in Spring Boot 3 BOM. No separate upgrade needed.
If you use a library that hasn't released a Jakarta-compatible version, you have three options: (1) fork and patch it yourself, (2) use the 'transformation' tool that repackages javax to jakarta at the bytecode level, or (3) stay on Spring Boot 2 until the library catches up.
Here's how to check if a library is Jakarta-compatible:
7. Testing Framework Changes: JUnit 5, Mockito, and Testcontainers
Spring Boot 3 uses JUnit 5.10+ and Mockito 5.x. If you were using JUnit 4 (via vintage engine), you need to migrate to JUnit 5 or add the vintage engine explicitly. Spring Boot 3's test starter no longer includes JUnit 4 by default.
Mockito 5.x changed its default mocking behavior. The lenient() mode is now required for unused stubs, and the doReturn() family of methods is deprecated in favor of when().thenReturn(). We had tests that passed in Mockito 4 but failed in Mockito 5 because of strict stubbing checks.
Testcontainers 1.18+ supports Jakarta. If you use Testcontainers for integration tests, update to the latest version. The JDBC URL syntax changed slightly โ you now need to use 'tc:' prefix instead of 'testcontainers:' for JDBC support.
Here's a typical test migration:
8. Monitoring and Observability: Actuator and Micrometer Changes
Spring Boot 3's Actuator and Micrometer received significant updates. If you rely on custom health indicators, metrics, or tracing, you need to update them.
First, the Actuator endpoint IDs changed. The 'health' endpoint is still there, but the 'info' endpoint now returns an empty response by default unless you configure it. The 'metrics' endpoint uses Micrometer 1.11+ which changed some metric names. For example, jvm.memory.used became jvm.memory.used? (the question mark is part of the name in Prometheus format).
Second, Micrometer Tracing (formerly Spring Cloud Sleuth) is now the default tracing solution. If you were using Sleuth, you need to migrate to Micrometer Tracing. The API is different: you now use ObservationRegistry instead of Tracer, and spans are created via Observation.createNotStarted().
Third, the Actuator's CORS configuration changed. In Spring Boot 2, you could configure CORS for Actuator endpoints via management.endpoints.web.cors.allowed-origins. In Spring Boot 3, this is deprecated in favor of a dedicated CorsConfiguration bean.
Here's the new way to expose all Actuator endpoints:
The Midnight Jakarta ClassNotFoundException
mvn dependency:tree -Dincludes=javax.servlet to find all offenders.- Always run a full dependency tree analysis for javax.* packages before deploying to production.
- Do not trust 'compatible' labels โ compile-test every third-party library in a staging environment.
- Roll out canary deployments with 5% traffic and monitor for silent failures, not just HTTP 500s.
mvn dependency:tree -Dincludes=javax.servlet to find the offending dependency. Exclude it or update to a Jakarta-compatible version. Add an exclusion in your POM.curl /actuator/metrics to list all available metrics. Update your Prometheus recording rules or Grafana dashboard queries to match new metric names.mvn dependency:tree -Dincludes=javax.*mvn dependency:tree -Dincludes=jakarta.*| File | Command / Code | Purpose |
|---|---|---|
| UserRepository.java | @Entity | 1. The Jakarta EE 9 Package Rename |
| pom.xml | 2. What the Official Docs Won't Tell You | |
| SecurityConfig.java | @Configuration | 3. Spring Security 6 |
| application.properties | spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.Ca... | 4. Hibernate 6 |
| pom.xml | 5. Dependency and Build Tool Changes | |
| DependencyCheck.java | public class DependencyCheck { | 6. Third-Party Library Compatibility |
| UserServiceTest.java | @RunWith(SpringRunner.class) | 7. Testing Framework Changes |
| application.yml | management: | 8. Monitoring and Observability |
Key takeaways
Interview Questions on This Topic
Explain the javax to jakarta package rename and why it was necessary.
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Spring Boot. Mark it forged?
6 min read · try the examples if you haven't