---
# === IDENTITY ===
id: software/migrations/java-ee-to-spring-boot/2026
canonical_question: "How do I migrate from Java EE to Spring Boot?"
aliases:
  - "Java EE to Spring Boot migration guide"
  - "migrate EJB to Spring Boot"
  - "Jakarta EE to Spring Boot migration"
  - "convert Java EE application to Spring Boot"
  - "replace WebLogic JBoss with Spring Boot"
  - "Java EE modernization to Spring Boot"
  - "enterprise Java to Spring Boot migration"
  - "javax to jakarta migration Spring Boot"
entity_type: software_reference
domain: software > migrations > java_ee_to_spring_boot
region: global
jurisdiction: global
temporal_scope: 2017-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.93
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: evolving
  last_breaking_change: "Spring Boot 4.0.6 (2026-04-23) — Jakarta EE 11, Spring Framework 7, Jackson 3.x (tools.jackson group), JUnit 6"
  next_review: 2026-11-13
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Spring Boot 3+ requires Java 17 minimum — upgrade JDK before starting migration"
  - "Spring Boot 4.0 requires Jakarta EE 11 alignment — all javax.* imports must become jakarta.*"
  - "Spring Boot 4.0 Jackson 3 changed group ID from com.fasterxml.jackson to tools.jackson — every Jackson dependency coordinate must be updated"
  - "Never run javax.* and jakarta.* imports in the same compilation unit — mixed namespaces cause ClassNotFoundException at runtime"
  - "OpenRewrite automates ~80% of namespace migration — reflection-based javax.* loading (Class.forName) is NOT caught by automated tools"
  - "Container-managed transactions (CMT) in EJBs are implicit — Spring requires explicit @Transactional annotations or data silently does not persist"
  - "ReactDOM.render()-style big-bang migration is an anti-pattern — use Strangler Fig pattern to migrate module by module"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "User wants to migrate between Spring Boot versions (e.g., 2.x to 3.x)"
    use_instead: "software/migrations/spring-boot-2-to-3/2026"
  - condition: "User wants to stay on Jakarta EE and move to a modern runtime like Quarkus"
    use_instead: "software/migrations/java-ee-to-quarkus/2026"
  - condition: "User wants to upgrade Java versions only (e.g., Java 8 to Java 21) without framework change"
    use_instead: "software/migrations/java-8-to-java-21/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: source_java_ee_version
    question: "Which Java EE / Jakarta EE version is the app running on?"
    type: choice
    options: ["Java EE 5-7 (javax.*)", "Java EE 8 / Jakarta EE 8 (javax.*)", "Jakarta EE 9-10 (jakarta.*)"]
  - key: app_server
    question: "Which application server is deployed?"
    type: choice
    options: ["WebLogic", "JBoss/WildFly", "WebSphere", "GlassFish/Payara", "Other"]
  - key: migration_target
    question: "Which Spring Boot version are you targeting?"
    type: choice
    options: ["Spring Boot 3.5.x (OSS support ends 2026-06-30)", "Spring Boot 4.0.x (current GA, supported through 2026-12-31)", "Spring Boot 4.1 (RC1 available 2026-04-23)"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/migrations/java-ee-to-spring-boot/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  depends_on:
    - id: "software/migrations/java-8-to-java-21/2026"
      label: "Java 8 to Java 21 Migration"
  related_to:
    - id: "software/migrations/spring-boot-2-to-3/2026"
      label: "Spring Boot 2 to Spring Boot 3 Migration"
    - id: "software/migrations/monolith-to-microservices/2026"
      label: "Monolith to Microservices Migration"
    - id: "software/migrations/jsf-to-thymeleaf/2026"
      label: "JSF to Thymeleaf Migration"
  solves:
    - id: "software/migrations/weblogic-to-spring-boot/2026"
      label: "WebLogic to Spring Boot Migration"
  alternative_to:
    - id: "software/migrations/java-ee-to-quarkus/2026"
      label: "Java EE to Quarkus Migration"
  often_confused_with:
    - id: "software/migrations/spring-boot-2-to-3/2026"
      label: "Spring Boot 2 to Spring Boot 3 Migration (version upgrade, not framework change)"

# === SOURCES (8 authoritative sources) ===
# Types: official_docs, technical_blog, rfc_spec, academic_paper, community_resource, industry_report
# Reliability: high, moderate_high, moderate, moderate_low, low, authoritative
sources:
  - id: src1
    title: "Spring Boot 3.0 Migration Guide"
    author: Spring Projects
    url: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide
    type: official_docs
    published: 2022-11-24
    reliability: authoritative
  - id: src2
    title: "Migrating from Java EE to Spring Boot"
    author: TwinDB
    url: https://twin.sh/articles/14/migrating-from-java-ee-to-spring-boot
    type: technical_blog
    published: 2023-05-15
    reliability: high
  - id: src3
    title: "Java EE to Spring Boot Migration Strategy for 2025"
    author: LegacyLeap
    url: https://www.legacyleap.ai/blog/java-ee-to-spring-boot-migration/
    type: industry_report
    published: 2025-03-10
    reliability: high
  - id: src4
    title: "EJB to Spring Boot Migration Guide 2026"
    author: LegacyLeap
    url: https://www.legacyleap.ai/blog/ejb-to-spring-boot-migration/
    type: industry_report
    published: 2025-11-20
    reliability: high
  - id: src5
    title: "Spring Boot 3 and the Move to Jakarta EE: What Developers Need to Know"
    author: Java Code Geeks
    url: https://www.javacodegeeks.com/2024/12/spring-boot-3-and-the-move-to-jakarta-ee-what-developers-need-to-know.html
    type: community_resource
    published: 2024-12-01
    reliability: moderate_high
  - id: src6
    title: "Enterprise Java Migration 2026: Modernize Legacy Stacks"
    author: LegacyLeap
    url: https://www.legacyleap.ai/blog/java-migration-guide/
    type: industry_report
    published: 2025-12-15
    reliability: high
  - id: src7
    title: "Integration Guide for Spring and EJB"
    author: Baeldung
    url: https://www.baeldung.com/spring-ejb
    type: technical_blog
    published: 2024-06-01
    reliability: high
  - id: src8
    title: "Spring Boot 4.0 Release Notes"
    author: Spring Projects
    url: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes
    type: official_docs
    published: 2025-11-20
    reliability: authoritative
  - id: src9
    title: "Spring Boot 4.0 Migration Guide"
    author: Spring Projects
    url: https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide
    type: official_docs
    published: 2026-04-23
    reliability: authoritative
  - id: src10
    title: "Spring Boot 4 Migration Guide: Faster, Safer, at Scale"
    author: Moderne
    url: https://www.moderne.ai/blog/spring-boot-4x-migration-guide
    type: technical_blog
    published: 2026-03-12
    reliability: high
  - id: src11
    title: "Spring Boot Versions, EOL Dates, and Latest Releases (April 2026)"
    author: HeroDevs
    url: https://www.herodevs.com/blog-posts/spring-boot-versions-eol-dates-and-latest-releases-april-2026
    type: industry_report
    published: 2026-04-23
    reliability: high
---

# How to Migrate from Java EE to Spring Boot

## TL;DR

- **Bottom line**: Migrate incrementally using the Strangler Fig pattern -- convert EJBs to `@Service`/`@Component` Spring beans, replace JNDI with dependency injection, swap the app server for embedded Tomcat, and update `javax.*` imports to `jakarta.*` for Spring Boot 3+.
- **Key tool/command**: `@Service` replaces `@Stateless`, `@Transactional` replaces container-managed transactions, `application.properties` replaces JNDI/XML config. Use OpenRewrite `JavaxMigrationToJakarta` recipe for automated namespace migration.
- **Watch out for**: The `javax.*` to `jakarta.*` namespace change in Spring Boot 3+ breaks every import -- use OpenRewrite to automate, not manual find-and-replace. Spring Boot 4.0 now requires Jakarta EE 11 alignment.
- **Works with**: Spring Boot 3.5.14 (final 3.x, OSS support ends 2026-06-30) or Spring Boot 4.0.6 (current GA, supported through 2026-12-31; Java 17 minimum, Java 25 recommended), migrating from any Java EE 5-8 / Jakarta EE 8-10 app server (WebLogic, JBoss/WildFly, GlassFish, WebSphere). [src9, src11]

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Spring Boot 3+ requires Java 17 as the minimum JDK version. If the Java EE app runs on Java 8 or 11, the JDK must be upgraded first, which may surface deprecated API issues (`javax.xml.bind`, reflection access). [src1]
- All `javax.*` Java EE imports must become `jakarta.*` for Spring Boot 3+. There is no partial migration -- mixed namespaces in the same compilation unit cause `ClassNotFoundException` at runtime. [src5]
- OpenRewrite's `JavaxMigrationToJakarta` recipe automates ~80% of the namespace change, but code using reflection to load `javax.*` classes (e.g., `Class.forName("javax.persistence.Entity")`) or string-based JNDI lookups will not be caught. Manual audit required. [src1]
- Container-managed transactions (CMT) in EJBs apply to every business method implicitly. Spring requires explicit `@Transactional` annotations -- omitting them silently disables transaction management and can cause partial commits. [src2]
- Spring Boot 4.0 (current GA 4.0.6, April 2026) requires Jakarta EE 11 alignment, Jackson 3.x, and JUnit 6. Jackson 3 also moved its Maven group ID from `com.fasterxml.jackson` to `tools.jackson`, renamed `@JsonComponent`/`@JsonMixin` to `@JacksonComponent`/`@JacksonMixin`, and reorganized properties under `spring.jackson.json.read` / `spring.jackson.json.write`. [src8, src9, src10]
- Never attempt a big-bang rewrite of the entire application at once. Use the Strangler Fig pattern to migrate one module at a time, keeping both systems running in parallel until each module is validated in production. [src3]
- Spring Boot 4.0 replaces `@MockBean`/`@SpyBean` with `@MockitoBean`/`@MockitoSpyBean`, removes `MockitoTestExecutionListener`, and stops auto-configuring MockMVC/WebClient inside `@SpringBootTest`. Test suites that compiled clean on 3.x will fail to compile on 4.x without these annotation renames. [src9, src10]

## Quick Reference

| Java EE Pattern | Spring Boot Equivalent | Example |
|---|---|---|
| `@Stateless` EJB | `@Service` | `@Service public class OrderService { ... }` |
| `@Stateful` EJB | `@Service` + `@SessionScope` | `@Service @SessionScope public class CartService { ... }` |
| `@Singleton` EJB | `@Service` (default singleton) | Spring beans are singleton-scoped by default |
| `@MessageDriven` (JMS) | `@JmsListener` | `@JmsListener(destination = "orders") void process(Message m)` |
| `@EJB` injection | `@Autowired` or constructor injection | `public OrderController(OrderService svc) { this.svc = svc; }` |
| `@PersistenceContext` | `@Autowired EntityManager` or Spring Data JPA | `public interface OrderRepo extends JpaRepository<Order, Long> {}` |
| `@Resource` JNDI lookup | `@Value` + `application.properties` | `@Value("${datasource.url}") String dbUrl;` |
| `@TransactionAttribute` | `@Transactional` | `@Transactional(propagation = REQUIRED)` |
| `@Interceptors` / `@AroundInvoke` | Spring AOP `@Aspect` + `@Around` | `@Around("execution(* com.app.service.*.*(..))")` |
| `@Schedule` (EJB Timer) | `@Scheduled` | `@Scheduled(cron = "0 0 * * * *") void hourlyJob()` |
| JSF Managed Beans | Spring MVC `@Controller` + Thymeleaf | `@Controller public class OrderController { ... }` |
| `web.xml` Servlet config | `@SpringBootApplication` auto-config | Remove `web.xml`, rely on embedded Tomcat |
| `persistence.xml` | `application.properties` + auto-config | `spring.datasource.url=jdbc:postgresql://...` |
| `ejb-jar.xml` deployment descriptor | Annotation-driven config | Delete descriptor, use `@Service`/`@Component` |
| JAAS Security | Spring Security `SecurityFilterChain` | `@Bean SecurityFilterChain filterChain(HttpSecurity http)` |

## Decision Tree

```
START
├── Is the app running on Java EE 5-7 (javax.*)?
│   ├── YES → Target Spring Boot 3.x (requires javax→jakarta namespace change)
│   └── NO (already Jakarta EE 9+) → Direct Spring Boot 3.x/4.x migration
├── Does the app use Stateful EJBs or extended persistence contexts?
│   ├── YES → Extract session state to Redis/DB-backed Spring Session first
│   └── NO ↓
├── Does the app use JSF for the frontend?
│   ├── YES → Replace with Thymeleaf (server-side) or REST API + SPA (React/Angular)
│   └── NO ↓
├── Are there remote EJB calls between services?
│   ├── YES → Replace with REST/gRPC endpoints (Spring WebClient/RestClient)
│   └── NO ↓
├── Does the app rely on vendor-specific features (WebLogic JMS, JTA, JCA)?
│   ├── YES → Replace with Spring-compatible alternatives (Spring JMS, Spring TX, REST)
│   └── NO ↓
├── Are you targeting Spring Boot 4.0?
│   ├── YES → Ensure Jakarta EE 11, Jackson 3.x, JUnit 6 alignment
│   └── NO (3.5.x) → Jakarta EE 10 sufficient
└── DEFAULT → Convert EJBs to @Service, replace JNDI with @Value/ConfigProperties,
              add spring-boot-starter-* dependencies, remove app server
```

## Step-by-Step Guide

### 1. Audit the Java EE surface area

Inventory every EJB, JNDI lookup, Java EE API usage, and vendor-specific feature before writing any code. This converts a vague migration into discrete, estimable tasks. [src3]

```bash
# Count EJB annotations
grep -rn '@Stateless\|@Stateful\|@Singleton\|@MessageDriven' --include='*.java' | wc -l

# Count JNDI lookups
grep -rn 'InitialContext\|@Resource\|lookup(' --include='*.java' | wc -l

# Count Java EE imports (javax.* that need jakarta.* change)
grep -rn 'import javax\.' --include='*.java' | wc -l

# Count JSF usage
grep -rn '@ManagedBean\|@FacesConverter\|@FacesValidator' --include='*.java' | wc -l
find . -name '*.xhtml' -o -name '*.jsf' | wc -l
```

**Verify**: A typical mid-size Java EE app has 20-100 EJBs, 50-200 JNDI lookups, and 500+ javax imports. Prioritize stateless EJBs for early wins.

### 2. Set up the Spring Boot project alongside the existing app

Create a new Spring Boot project that will gradually absorb the Java EE codebase. Both can coexist during migration. [src1, src2]

```xml
<!-- pom.xml for Spring Boot 3.5.14 (final 3.x; OSS support ends 2026-06-30) -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.14</version>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
</dependencies>
```

```java
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

**Verify**: `mvn spring-boot:run` starts embedded Tomcat on port 8080 with no errors.

### 3. Replace JNDI and XML configuration with application.properties

Migrate all JNDI datasource lookups and XML descriptor configurations to Spring Boot's externalized configuration. [src2, src6]

```properties
# application.properties — replaces persistence.xml + JNDI datasource
spring.datasource.url=jdbc:postgresql://localhost:5432/myapp
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.postgresql.Driver

# JPA/Hibernate (replaces persistence.xml properties)
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.open-in-view=false

# JMS (replaces JNDI JMS connection factory)
spring.activemq.broker-url=tcp://localhost:61616
spring.activemq.user=${JMS_USERNAME}
spring.activemq.password=${JMS_PASSWORD}
```

**Verify**: `spring.datasource.*` properties connect to the same database the Java EE app uses. Run `SELECT 1` on startup.

### 4. Convert EJBs to Spring services

Replace EJB annotations with Spring equivalents. Start with stateless beans -- they have the fewest dependencies. [src2, src4]

```java
// BEFORE: Java EE Stateless EJB
@Stateless
public class OrderServiceBean implements OrderService {
    @PersistenceContext
    private EntityManager em;

    @EJB
    private InventoryService inventoryService;

    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Order createOrder(OrderRequest request) {
        Inventory inv = inventoryService.reserve(request.getProductId(), request.getQty());
        Order order = new Order(request, inv);
        em.persist(order);
        return order;
    }
}

// AFTER: Spring Boot Service
@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepo;
    private final InventoryService inventoryService;

    public OrderService(OrderRepository orderRepo, InventoryService inventoryService) {
        this.orderRepo = orderRepo;
        this.inventoryService = inventoryService;
    }

    public Order createOrder(OrderRequest request) {
        Inventory inv = inventoryService.reserve(request.getProductId(), request.getQty());
        Order order = new Order(request, inv);
        return orderRepo.save(order);
    }
}
```

**Verify**: Run unit tests for each converted service. Constructor injection ensures missing dependencies fail fast at startup.

### 5. Replace JPA EntityManager with Spring Data repositories

Convert manual EntityManager queries to Spring Data JPA repositories with derived query methods. [src2]

```java
// BEFORE: Java EE DAO with EntityManager
@Stateless
public class OrderDAO {
    @PersistenceContext
    private EntityManager em;

    public List<Order> findByCustomer(Long customerId) {
        return em.createQuery(
            "SELECT o FROM Order o WHERE o.customerId = :cid", Order.class)
            .setParameter("cid", customerId)
            .getResultList();
    }
}

// AFTER: Spring Data JPA Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
    List<Order> findByCustomerId(Long customerId);

    @Query("SELECT o FROM Order o WHERE o.status = :status AND o.createdAt > :since")
    List<Order> findRecentByStatus(@Param("status") OrderStatus status,
                                    @Param("since") LocalDateTime since);
}
```

**Verify**: `orderRepository.findByCustomerId(1L)` returns the same results as the old DAO. Compare SQL output with `spring.jpa.show-sql=true`.

### 6. Migrate security from JAAS to Spring Security

Replace Java EE container-managed security (JAAS, `web.xml` security constraints) with Spring Security's filter chain. [src6]

```java
// BEFORE: web.xml security constraint
// <security-constraint>
//   <web-resource-collection>
//     <url-pattern>/admin/*</url-pattern>
//   </web-resource-collection>
//   <auth-constraint>
//     <role-name>ADMIN</role-name>
//   </auth-constraint>
// </security-constraint>

// AFTER: Spring Security configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/**").authenticated()
                .anyRequest().permitAll()
            )
            .formLogin(Customizer.withDefaults())
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
```

**Verify**: Access `/admin/dashboard` without authentication returns 401/302. Login with ADMIN role grants access.

### 7. Update javax.* imports to jakarta.* and remove the app server

The final step: update all namespace imports, remove app server deployment descriptors, and deploy as a standalone JAR. [src1, src5]

```bash
# Automated namespace migration using OpenRewrite
mvn -U org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta

# Or use IntelliJ IDEA: Refactor > Migrate Packages and Classes > javax to jakarta

# Delete deployment descriptors
rm -f src/main/webapp/WEB-INF/web.xml
rm -f src/main/webapp/WEB-INF/ejb-jar.xml
rm -f src/main/resources/META-INF/persistence.xml

# Build standalone JAR (no WAR, no app server)
mvn clean package -DskipTests
java -jar target/myapp-1.0.0.jar
```

**Verify**: `java -jar target/myapp.jar` starts successfully. No WebLogic/JBoss/GlassFish process needed. All API endpoints return identical responses.

## Code Examples

### Java: Converting a Stateless EJB to Spring Service

> Full script: [java-converting-a-stateless-ejb-to-spring-service.java](scripts/java-converting-a-stateless-ejb-to-spring-service.java) (48 lines)

```java
// Input:  A Java EE stateless EJB with JNDI, EntityManager, and CMT
// Output: Equivalent Spring Boot service with constructor injection
// === BEFORE: Java EE ===
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
# ... (see full script)
```

### Java: Converting Message-Driven Bean to Spring JMS Listener

> Full script: [java-converting-message-driven-bean-to-spring-jms-.java](scripts/java-converting-message-driven-bean-to-spring-jms-.java) (47 lines)

```java
// Input:  A Java EE MDB that processes JMS messages
// Output: Equivalent Spring Boot JMS listener
// === BEFORE: Java EE MDB ===
import javax.ejb.MessageDriven;
import javax.ejb.ActivationConfigProperty;
# ... (see full script)
```

### Java: Converting EJB Interceptors to Spring AOP

> Full script: [java-converting-ejb-interceptors-to-spring-aop.java](scripts/java-converting-ejb-interceptors-to-spring-aop.java) (38 lines)

```java
// Input:  A Java EE interceptor that logs method execution time
// Output: Equivalent Spring AOP aspect
// === BEFORE: Java EE Interceptor ===
import javax.interceptor.AroundInvoke;
import javax.interceptor.Interceptor;
# ... (see full script)
```

## Anti-Patterns

### Wrong: Manual JNDI lookups inside Spring beans

```java
// ❌ BAD — JNDI lookup in a Spring-managed bean defeats dependency injection
@Service
public class OrderService {
    public DataSource getDataSource() throws NamingException {
        InitialContext ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/MyDB");
    }
}
```

### Correct: Use Spring's dependency injection and configuration

```java
// ✅ GOOD — Spring auto-configures the DataSource from application.properties
@Service
public class OrderService {
    private final JdbcTemplate jdbcTemplate;

    public OrderService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }
}
// application.properties: spring.datasource.url=jdbc:postgresql://...
```

### Wrong: Mixing javax.* and jakarta.* imports in the same project

```java
// ❌ BAD — Using old javax imports with Spring Boot 3+
import javax.persistence.Entity;        // WRONG for Spring Boot 3+
import javax.persistence.Id;
import jakarta.validation.constraints.NotNull;  // Different namespace!

@Entity
public class Order {
    @Id
    private Long id;
    @NotNull
    private String name;
}
```

### Correct: Use jakarta.* exclusively with Spring Boot 3+

```java
// ✅ GOOD — Consistent jakarta.* namespace
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.validation.constraints.NotNull;

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @NotNull
    private String name;
}
```

### Wrong: Keeping the WAR packaging and external app server

```xml
<!-- ❌ BAD — Still deploying as WAR to external Tomcat/JBoss -->
<packaging>war</packaging>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>  <!-- Excluded for external server -->
</dependency>
```

### Correct: Use standalone JAR with embedded server

```xml
<!-- ✅ GOOD — Standalone executable JAR with embedded Tomcat -->
<packaging>jar</packaging>
<!-- spring-boot-starter-web already includes embedded Tomcat -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
```

### Wrong: Converting EJB interfaces to Spring interfaces unnecessarily

```java
// ❌ BAD — Creating unnecessary interfaces (leftover from EJB remote/local pattern)
public interface OrderServiceLocal {
    Order createOrder(OrderRequest request);
}
public interface OrderServiceRemote extends OrderServiceLocal {}

@Service
public class OrderServiceImpl implements OrderServiceLocal {
    public Order createOrder(OrderRequest request) { ... }
}
```

### Correct: Use concrete service classes unless you need polymorphism

```java
// ✅ GOOD — No interface needed unless you have multiple implementations
@Service
public class OrderService {
    public Order createOrder(OrderRequest request) { ... }
}
```

### Wrong: Big-bang migration of the entire application

```java
// ❌ BAD — Attempting to rewrite everything at once
// - 6+ months of development with zero deployable output
// - No regression testing against live behavior
// - Business logic drift between old and new codebases
```

### Correct: Strangler Fig pattern with incremental migration

```java
// ✅ GOOD — Migrate one module at a time, deploy each to production
// Phase 1: OrderService EJB → Spring @Service (2 weeks)
// Phase 2: PaymentService EJB → Spring @Service (2 weeks)
// Phase 3: JMS MDBs → Spring @JmsListener (1 week)
// Phase 4: JSF → Thymeleaf or REST API (4 weeks)
// Phase 5: Remove app server, deploy as standalone JAR
// Each phase is independently deployable and testable
```

## Common Pitfalls

- **javax.* imports not updated to jakarta.***: Spring Boot 3+ uses Jakarta EE 9+ exclusively. Every `javax.persistence`, `javax.servlet`, `javax.validation` import must become `jakarta.*`. Fix: Run OpenRewrite recipe `JavaxMigrationToJakarta` before compiling. [src1, src5]
- **Hibernate dialect misconfiguration after upgrade**: Hibernate 6 (used in Spring Boot 3) changed dialect auto-detection and removed legacy dialects. Fix: Remove explicit `hibernate.dialect` from properties; let Hibernate auto-detect, or use `org.hibernate.dialect.PostgreSQLDialect` (no version suffix). [src1]
- **Stateful EJB session state lost in Spring**: Spring beans are singleton-scoped by default, so `@Stateful` EJB state patterns break silently. Fix: Use `@SessionScope` for HTTP session state or externalize to Redis via Spring Session. [src4]
- **JNDI lookups fail at runtime**: Spring Boot has no JNDI context by default. Any remaining `InitialContext.lookup()` calls throw `NamingException`. Fix: Replace all JNDI lookups with `@Value`, `@ConfigurationProperties`, or Spring-managed beans. [src3]
- **EJB remote interfaces cause ClassNotFoundException**: Remote EJB stubs depend on app server classloader. Fix: Replace remote EJB calls with REST endpoints using `RestClient` or `WebClient`. [src4]
- **Transaction propagation behaves differently**: EJB defaults to `REQUIRED` for all methods; Spring only applies `@Transactional` where annotated. Fix: Add `@Transactional` to service class or individual methods. Missing it causes data not persisting. [src2]
- **web.xml filters not picked up**: Spring Boot ignores `web.xml` by default. Servlet filters, listeners, and URL mappings defined there are silently lost. Fix: Register as `@Bean FilterRegistrationBean` or use `@WebFilter` with `@ServletComponentScan`. [src1]
- **Vendor-specific JMS/JCA not portable**: WebLogic T3, JBoss HornetQ, or WebSphere SIBus JMS configurations don't work outside their app server. Fix: Switch to ActiveMQ Artemis, RabbitMQ, or Kafka with Spring Boot starters. [src6]
- **Spring Boot 4.0 Jackson 3.x breaking changes**: Jackson 3 moved its Maven group ID from `com.fasterxml.jackson` to `tools.jackson`, renamed `JsonObjectSerializer` → `ObjectValueSerializer`, `@JsonComponent` → `@JacksonComponent`, `@JsonMixin` → `@JacksonMixin`, and regrouped properties under `spring.jackson.json.read.*` / `spring.jackson.json.write.*`. Fix: update every Jackson dependency coordinate, rename the four annotations, and rerun integration tests for custom serializers. [src8, src9]

## Diagnostic Commands

```bash
# Find remaining javax.* imports (must be zero for Spring Boot 3+)
grep -rn 'import javax\.' --include='*.java' | grep -v 'javax.crypto\|javax.net\|javax.xml' | wc -l

# Find remaining EJB annotations (must be zero after migration)
grep -rn '@Stateless\|@Stateful\|@MessageDriven\|@Singleton' --include='*.java' | wc -l

# Find JNDI lookups (must be zero)
grep -rn 'InitialContext\|lookup(' --include='*.java' | wc -l

# Find deployment descriptors (should be deleted)
find . -name 'web.xml' -o -name 'ejb-jar.xml' -o -name 'persistence.xml' -o -name 'application.xml'

# Verify Spring Boot starts cleanly
mvn spring-boot:run 2>&1 | grep -E 'Started|ERROR|WARN'

# Check for dependency conflicts (javax vs jakarta)
mvn dependency:tree | grep -i 'javax\.\|jakarta\.'

# Verify OpenRewrite migration completeness
mvn -U org.openrewrite.maven:rewrite-maven-plugin:dryRun \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-migrate-java:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta

# Check Spring Boot 4.0 compatibility (if targeting 4.x)
mvn -U org.openrewrite.maven:rewrite-maven-plugin:dryRun \
  -Drewrite.recipeArtifactCoordinates=org.openrewrite.recipe:rewrite-spring:RELEASE \
  -Drewrite.activeRecipes=org.openrewrite.java.spring.boot4.UpgradeSpringBoot_4_0
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Spring Boot 4.1 (2026) | RC1 released 2026-04-23 | Continues 4.x line | Pre-GA — pilot only |
| Spring Boot 4.0.6 (Apr 2026) | Current GA (OSS support through 2026-12-31) | Jakarta EE 11, Spring Framework 7, Jackson 3.x (group `tools.jackson`), JUnit 6, modular starters, Undertow removed | Java 17 min / Java 25 recommended; virtual threads on by default for blocking I/O; `@MockitoBean` replaces `@MockBean`; API versioning in @RequestMapping |
| Spring Boot 3.5.14 (Apr 2026) | Active (OSS support ends 2026-06-30; 5-year commercial extension via spring.io) | Final 3.x line | Move to 3.5.x before any 4.x jump — clears deprecations |
| Spring Boot 3.4 (2024) | EOL | RestClient replaces RestTemplate | Upgrade to 3.5.x first |
| Spring Boot 3.0 (2022) | EOL | javax.* to jakarta.*, Java 17+, Hibernate 6 | Use OpenRewrite + spring-boot-properties-migrator |
| Spring Boot 2.7 (2022) | EOL Nov 2023 | Last version with javax.* support | Upgrade to 2.7 first as stepping stone to 3.x |
| Java EE 8 / Jakarta EE 8 | Maintenance | Last version with javax.* namespace | Direct migration target for Spring Boot 2.7 |
| Jakarta EE 9-10 | Active | javax.* renamed to jakarta.* | Aligned with Spring Boot 3.x namespace |
| Jakarta EE 11 | Current | Servlet 6.1 baseline, additional API updates | Required for Spring Boot 4.0 |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| App server licensing costs are unsustainable (WebLogic/WebSphere) | Java EE app is small (<5 EJBs) and stable | Stay on Jakarta EE with WildFly/Payara |
| Team needs cloud-native deployment (Docker, K8s) | App is scheduled for retirement within 12 months | Keep legacy, invest in documentation |
| Hiring Spring developers is easier than Java EE specialists | Heavy use of JCA connectors or proprietary app server APIs | Consider Quarkus (closer to Jakarta EE) |
| Need microservices decomposition path | Team has no Spring experience and tight deadline | Train first, then migrate |
| Existing app server is EOL (JBoss AS 7, GlassFish 4) | App relies on EJB remote calls across many services | Consider service mesh or API gateway first |
| Want virtual threads and modern observability (Spring Boot 4.0) | Must maintain compatibility with Java 8/11 | Stay on Jakarta EE or use Quarkus with older Java |

## Important Caveats

- Spring Boot 3+ requires Java 17 minimum. If your Java EE app runs on Java 8, you must upgrade the JDK first. This may surface issues with deprecated APIs (`java.util.Date`, `javax.xml.bind`, reflection access).
- The `javax.*` to `jakarta.*` namespace change affects ALL Java EE APIs: Servlet, JPA, Bean Validation, CDI, JMS, JAX-RS. Third-party libraries that depend on `javax.*` (older Hibernate, Jersey, Apache CXF) must also be upgraded to jakarta-compatible versions.
- OpenRewrite can automate ~80% of the namespace migration, but custom code using reflection to load `javax.*` classes (e.g., `Class.forName("javax.persistence.Entity")`) will not be caught by automated tools.
- Container-managed transactions (CMT) in EJBs are implicit -- every business method is transactional. Spring requires explicit `@Transactional` annotations. Omitting them silently disables transaction management, which can cause partial commits.
- EJB `@Schedule` uses cron-like but different syntax than Spring's `@Scheduled`. Double-check cron expressions during migration. Spring uses 6 fields (second, minute, hour, day, month, weekday); Java EE uses different field orders.
- Spring Boot 4.0 (current GA 4.0.6, April 2026) introduces Jakarta EE 11, Jackson 3.x (group ID moved from `com.fasterxml.jackson` to `tools.jackson`), JUnit 6, modular starters, and removes Undertow / Spock / `MockitoTestExecutionListener`. If migrating directly from Java EE, do not jump straight to 4.0 — land on 3.5.14 first to clear deprecations, then run the OpenRewrite `UpgradeSpringBoot_4_0` composite recipe. Spring 3.5 OSS support ends 2026-06-30, so plan the 3.5 → 4.0 jump before that date or buy commercial extended support. [src9, src10, src11]

## Related Units

- [Java 8 to Java 21 Migration](/software/migrations/java-8-to-java-21/2026)
- [Spring Boot 2 to Spring Boot 3 Migration](/software/migrations/spring-boot-2-to-3/2026)
- [Monolith to Microservices Migration](/software/migrations/monolith-to-microservices/2026)
- [WebLogic to Spring Boot Migration](/software/migrations/weblogic-to-spring-boot/2026)
- [JSF to Thymeleaf Migration](/software/migrations/jsf-to-thymeleaf/2026)
- [Java EE to Quarkus Migration](/software/migrations/java-ee-to-quarkus/2026)
