From Java EE to Spring Boot 3: A Pragmatic Migration Strategy
Your WebLogic monolith has been running the same payroll system since 2014. Java 8. EJB 3.1. A 300,000-line codebase that nobody wants to touch. Then Oracle announces extended support will cost 6x more after 2026, and your CISO flags the unmaintained security vulnerabilities in your Java EE stack.
The "Big Bang" rewrite is the siren song of software engineering. It promises a fresh start, but usually delivers a multi-year feature freeze and a project that is obsolete by launch.
For enterprise teams maintaining Java EE (J2EE) monoliths, the path forward isn't a rewrite—it's an incremental migration. With Spring Boot 3 requiring Java 17 and Jakarta EE 9, the technical gap is real. But the tooling to bridge it has matured significantly.
This guide outlines a pragmatic, risk-averse strategy for migrating legacy Java EE applications to Spring Boot 3 using the Strangler Fig pattern—validated across fintech and healthcare migrations where downtime isn't an option.
The Strangler Fig Pattern: Tactical Implementation
Coined by Martin Fowler, the Strangler Fig pattern involves creating a new system around the edges of the old, gradually letting it grow and take over until the old system can be strangled (decommissioned).
For a Java EE to Spring Boot migration, this means:
- Identify a vertical slice of functionality (e.g., the "User Profile" module).
- Implement this slice in a new Spring Boot application.
- Route traffic for that specific slice to the new application via a proxy (like NGINX or Spring Cloud Gateway).
- Monitor for feature parity and performance regressions.
- Repeat until the monolith is empty.
Choosing Your First Slice
Not all modules are equal migration candidates. Prioritize by:
- Low Coupling: Minimal dependencies on other monolith components.
- High Business Value: Visible impact to stakeholders (justifies the investment).
- Moderate Complexity: Not trivial (won't prove the pattern), but not the most tangled code either.
Good first candidates: Report generation, search/filtering, notification services. Avoid initially: Authentication, billing, or any module with complex distributed transactions.
The Jakarta EE Hurdle
Before writing a single line of Spring Boot 3 code, you must address the elephant in the room: the javax to jakarta namespace shift.
Spring Boot 3 is built on Spring Framework 6, which requires Jakarta EE 9 APIs. This means the familiar javax.servlet, javax.persistence (JPA), and javax.validation packages no longer exist in the Spring Boot 3 world. They have been renamed to jakarta.servlet, jakarta.persistence, etc.
The Migration Path
You cannot easily mix javax.* and jakarta.* libraries in the same runtime if they expect to share state (like an EntityManager).
- Option A: Big Bang Namespace Swap. Run a tool like OpenRewrite to update all imports across your entire codebase. This is risky for large monoliths.
- Option B: The Sidecar Approach. Keep the legacy app on Java EE 8 (using
javax), and build the new Spring Boot 3 app (usingjakarta) as a separate deployable. They communicate via REST or gRPC, not by sharing JARs. We recommend this approach.
Step-by-Step Migration Strategy
1. Establish the Proxy
Place a reverse proxy in front of your legacy application. Initially, it routes 100% of traffic to the legacy upstream.
location / {
proxy_pass http://legacy-app:8080;
}
2. Carve Out a Vertical Slice
Choose a domain with low coupling but high business value. Avoid the "Auth" service initially—it's too entangled. A "Reporting" or "Search" service is often a safer starting point.
Create a new Spring Boot 3 application:
<!-- Legacy Java EE pom.xml (Before) -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<!-- Spring Boot 3 pom.xml (After) -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- Includes jakarta.servlet-api 6.0 automatically -->
</dependency>
</dependencies>
3. The Anti-Corruption Layer (ACL)
Your new Spring Boot app will likely need data that still lives in the legacy database.
- Do not simply connect the new app to the old database schema if you can avoid it.
- Do create an Anti-Corruption Layer. If you must read from the legacy DB, map the legacy entities to clean, modern domain objects immediately. Do not let the legacy schema dictate your new domain model.
4. Switch the Traffic
Update your proxy to route specific paths to the new service.
location /api/v2/reports {
proxy_pass http://new-spring-boot-app:8081;
}
location / {
proxy_pass http://legacy-app:8080;
}
Common Pitfalls
Distributed Transactions (JTA)
Legacy EJBs often rely on JTA (Java Transaction API) for two-phase commits across multiple resources (DB + JMS). Spring Boot supports JTA, but in a microservices world, we prefer Sagas or Eventual Consistency. Avoid trying to replicate distributed transactions between the legacy app and the new app.
Shared Session State
If you are migrating a UI with server-side sessions (HttpSession), the user will lose their session when navigating between the legacy app and the new app.
- Solution: Externalize session state to Redis using Spring Session. Both the legacy app (via a filter) and the new app can share the Redis-backed session, provided they share a compatible serialization format (JSON is safer than Java Serialization here).
The "God Class" Dependency
You will inevitably find a utility class or DTO that is used everywhere.
- Don't just copy-paste it.
- Don't create a shared library JAR immediately (this couples deployment cycles).
- Do duplicate the code initially. De-coupling is more important than DRY (Don't Repeat Yourself) during a migration.
Testing Strategy: Ensuring Feature Parity
The biggest risk in any migration is breaking existing functionality. Here's how to mitigate it:
Shadow Traffic Testing
Run both the legacy endpoint and the new Spring Boot endpoint in parallel, comparing responses:
- Tool: Spring Cloud Gateway with custom filters, or Diffy (Twitter's open-source shadow proxy).
- Metric: 99%+ response match rate before cutting over traffic.
Contract Testing
Define API contracts using OpenAPI/Swagger, then validate that both implementations conform:
# Generate tests from OpenAPI spec
mvn verify -Dspring.cloud.contract.verifier.skip=false
Canary Deployments
Route 5% of production traffic to the new service, monitor error rates and latency, then gradually increase:
- Rollback trigger: Error rate > 0.5% or p99 latency > 2x baseline.
Observability: What to Monitor
You cannot migrate what you cannot measure. Implement these metrics before you start:
| Metric | Legacy (Java EE) | New (Spring Boot 3) | Alert Threshold |
|---|---|---|---|
| Response Time (p99) | Measure baseline | Compare | > 1.5x baseline |
| Error Rate | Measure baseline | Compare | > 2x baseline |
| JVM Heap Usage | Measure baseline | Compare | > 85% |
| Database Connection Pool | Monitor saturation | Monitor saturation | > 80% utilization |
| Traffic Distribution | 100% → 0% | 0% → 100% | Track during rollout |
Recommendation: Use Micrometer (built into Spring Boot 3) to export metrics to Prometheus, then visualize in Grafana. Include custom business metrics (e.g., "successful payment transactions") to validate behavioral equivalence.
When NOT to Migrate
Not every Java EE application should migrate to Spring Boot 3. Consider staying on Java EE (or Jakarta EE) if:
- Your app is stable and unmaintained. If it's not being actively developed and has no security vulnerabilities, the migration cost may not justify the benefit.
- You're heavily invested in vendor-specific features. WebLogic Coherence, JBoss Infinispan clusters, or WebSphere MQ integrations are expensive to replicate.
- You have budget for extended commercial support. Oracle, IBM, and Red Hat offer long-term support contracts for legacy Java EE runtimes.
Decision Rule: Migrate if you're actively developing new features, facing compliance pressure (e.g., PCI-DSS flagging outdated Java versions), or suffering from slow hiring due to legacy tech stack perception.
Measuring Success
Define these metrics before you start:
- Migration Velocity: Modules migrated per quarter.
- Deployment Frequency: Before vs. after (Spring Boot should improve CI/CD).
- Incident Rate: Production bugs introduced during migration.
- Recruitment Impact: Time-to-hire for Java engineers (Spring Boot broadens candidate pool).
Conclusion
Migrating from Java EE to Spring Boot 3 is not just a framework swap; it's an architectural shift. By leveraging the Strangler Fig pattern, respecting the boundaries between the javax and jakarta worlds, and implementing robust testing and observability from day one, you can modernize incrementally while maintaining production stability.
The goal is not to finish the migration as fast as possible, but to make the system better with every step—and prove that improvement with data.
Frequently Asked Questions
Can I run Spring Boot 3 on Java 11?
No. Spring Boot 3 requires Java 17 as a baseline. If your organization is stuck on Java 8 or 11, you must upgrade the JDK first, or target Spring Boot 2.7 (which is now end-of-life for OSS support).
How do I automate the javax to jakarta renaming?
We highly recommend using OpenRewrite. It has specific recipes for migrating from Java EE 8 to Jakarta EE 9, handling not just imports but also configuration files (like persistence.xml).
Should I convert EJBs to Spring Beans?
Yes. Stateless Session Beans (SLSB) map directly to Spring @Service classes. Message Driven Beans (MDB) map to @JmsListener endpoints. Stateful Session Beans are harder and usually require a redesign using external cache (Redis).
Is it better to use Gradle or Maven for the new project?
Both work perfectly. However, if your legacy build is a complex Ant or Maven build, switching to Gradle might introduce too many variables at once. Sticking to Maven for the first phase of migration can reduce cognitive load.
How do I handle database migrations if the legacy app and new app share the same DB?
Use a database migration tool like Flyway or Liquibase in both applications. Coordinate schema changes carefully: the legacy app must tolerate new columns (even if it doesn't use them), and the new app must tolerate missing columns during the transition. Consider using views or synonyms to abstract schema differences.
What about performance? Will Spring Boot 3 be faster than my tuned Java EE app?
Not automatically. Spring Boot 3 on Java 17 with virtual threads (Project Loom) can handle higher concurrency with lower memory overhead, but if your Java EE app has been performance-tuned for a decade, you'll need to apply similar optimizations to Spring Boot. The win is in developer productivity and modern tooling, not raw speed.