Spring Boot 3 requires Jakarta EE 9+ namespaces. Every javax.persistence becomes jakarta.persistence. Every javax.servlet becomes jakarta.servlet. It sounds simple. For small projects, it is.

For anything with a real dependency tree, the migration has teeth.

This guide covers what changed, why it changed, how to migrate step by step, and where the migration will surprise you.

Why javax Became jakarta

This wasn’t a Spring decision. It was an industry-level transition that Spring Boot 3 inherited.

In 2017, Oracle donated Java EE to the Eclipse Foundation. The specifications, the reference implementations, the TCKs. But Oracle kept the trademark on the javax namespace. The Eclipse Foundation couldn’t modify javax.* packages and release them under the Java EE brand.

So everything got renamed. Java EE became Jakarta EE. The javax.* enterprise packages became jakarta.*. Jakarta EE 9 was the first release under the new namespace, and it was purely a rename. No new features. Just the package swap.

Spring Boot 3.0, released in November 2022, adopted Jakarta EE 9+ as its minimum requirement. Spring Framework 6 made the same move. If you want to upgrade to Spring Boot 3, you have to make the namespace switch. There is no opt-out.

Here’s the thing: the packages that changed are only the ones that were part of Java EE. The javax packages that belong to the core JDK did not change:

Changed (Jakarta EE)Unchanged (JDK core)
javax.persistence -> jakarta.persistencejavax.sql stays javax.sql
javax.servlet -> jakarta.servletjavax.crypto stays javax.crypto
javax.validation -> jakarta.validationjavax.net stays javax.net
javax.annotation -> jakarta.annotationjavax.swing stays javax.swing
javax.inject -> jakarta.injectjavax.naming stays javax.naming

This distinction trips people up. Not every javax import needs to change. Only the enterprise ones.

What Actually Breaks

Before diving into the migration steps, it helps to know what you’re dealing with. Here’s a real-world inventory of the most common breakages in a typical Spring Boot application.

Import Statements

The obvious one. Every enterprise javax import in your source code needs to change.

Before:

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import javax.annotation.PostConstruct;

After:

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import jakarta.annotation.PostConstruct;

In your own code, this is straightforward. Your IDE’s find-and-replace handles it. But you’re not just compiling your own code.

Dependencies That Still Use javax

This is where most teams spend the real time. Your application compiles fine after the import swap, but then a transitive dependency pulls in the old javax.servlet-api and suddenly you have two Servlet APIs on the classpath. Or a library internally references javax.persistence.EntityManager and throws NoClassDefFoundError at runtime.

Common offenders:

  • Swagger/SpringFox — SpringFox has not been updated for Jakarta. You need to switch to SpringDoc OpenAPI 2.x.
  • Older Hibernate versions — Hibernate 6.x supports Jakarta. Hibernate 5.x does not.
  • Apache CXF — Older versions depend on javax.ws.rs. You need CXF 4.x for Jakarta.
  • Javax.mail — Replaced by jakarta.mail (Eclipse Angus).
  • JAXB runtime — If you’re using javax.xml.bind, you need the Jakarta-compatible JAXB implementation.

XML Configuration and Descriptor Files

If your project uses persistence.xml, web.xml, or beans.xml, the XML namespace URIs change too:

Before:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             version="2.2">
    <persistence-unit name="default">
        <!-- ... -->
    </persistence-unit>
</persistence>

After:

<persistence xmlns="https://jakarta.ee/xml/ns/persistence"
             version="3.0">
    <persistence-unit name="default">
        <!-- ... -->
    </persistence-unit>
</persistence>

Reflection and String-Based Class References

The trickiest category. If any code constructs class names as strings and loads them via reflection, automated tools will not find those references:

// This compiles fine but fails at runtime after migration
Class<?> clazz = Class.forName("javax.persistence.EntityManager");

These are rare in typical application code but show up in frameworks, test utilities, and custom annotation processors. They only surface at runtime.

Step-by-Step Migration Process

Step 1: Upgrade Your JDK

Spring Boot 3 requires Java 17 at minimum. If you’re still on Java 8 or 11, handle the JDK upgrade first. Trying to do both simultaneously creates a confusing debugging experience where you can’t tell which change broke what.

See The Complete Guide to Migrating from Java 8 to Java 17 for that process.

Step 2: Upgrade Spring Boot to the Latest 2.7.x

Before jumping to 3.x, make sure you’re on the latest Spring Boot 2.7.x release. The 2.7 line introduced deprecation warnings for everything that would break in 3.0. Running your test suite on 2.7 will surface warnings that serve as a migration checklist.

<!-- Start here before going to 3.x -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.18</version>
</parent>

Fix every deprecation warning. Each one is a preview of something that will break in 3.0.

Step 3: Update pom.xml Dependencies

When you’re ready to move to Spring Boot 3, update the parent POM and swap out any javax dependencies for their Jakarta equivalents.

<!-- Spring Boot 3.x parent -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.4.4</version>
</parent>

<dependencies>
    <!-- JPA: Hibernate 6.x is pulled in automatically by Spring Boot 3 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- Validation: now uses jakarta.validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!-- If you were using javax.servlet-api directly, remove it.
         Spring Boot 3 brings in jakarta.servlet-api transitively. -->

    <!-- Replace SpringFox with SpringDoc 2.x -->
    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>2.8.6</version>
    </dependency>

    <!-- If you use javax.mail -->
    <dependency>
        <groupId>org.eclipse.angus</groupId>
        <artifactId>angus-mail</artifactId>
        <version>2.0.3</version>
    </dependency>
</dependencies>

For Gradle projects, the same applies in build.gradle:

plugins {
    id 'org.springframework.boot' version '3.4.4'
    id 'io.spring.dependency-management' version '1.1.7'
    id 'java'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6'
}

Step 4: Rename Imports in Source Code

For small projects, IDE bulk refactoring works. In IntelliJ: Ctrl+Shift+R (or Cmd+Shift+R on Mac), enable regex, and replace javax\.(persistence|servlet|validation|annotation|inject|enterprise|transaction|ws) with the jakarta equivalent.

For anything larger than a handful of files, use OpenRewrite. It’s the better tool for this job.

Step 5: Run OpenRewrite for Automated Migration

OpenRewrite has specific recipes for the Spring Boot 3 migration that handle the javax-to-jakarta rename along with dozens of other breaking changes.

Add the OpenRewrite plugin to your build:

<plugin>
    <groupId>org.openrewrite.maven</groupId>
    <artifactId>rewrite-maven-plugin</artifactId>
    <version>5.46.0</version>
    <configuration>
        <activeRecipes>
            <recipe>org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_4</recipe>
        </activeRecipes>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.openrewrite.recipe</groupId>
            <artifactId>rewrite-spring</artifactId>
            <version>5.25.0</version>
        </dependency>
    </dependencies>
</plugin>

Then run it:

mvn rewrite:run

This single command handles:

  • javax.* to jakarta.* import renames
  • Spring Boot property key changes (e.g., spring.redis.* to spring.data.redis.*)
  • Deprecated API replacements
  • HttpStatus constant changes
  • Configuration annotation updates

Review the diff afterward. OpenRewrite is good, but it’s not omniscient. Look for any changes that seem wrong or any files it missed.

For Gradle:

plugins {
    id 'org.openrewrite.rewrite' version '7.3.1'
}

rewrite {
    activeRecipe 'org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_4'
}

dependencies {
    rewrite 'org.openrewrite.recipe:rewrite-spring:5.25.0'
}
gradle rewriteRun

Step 6: Audit Third-Party Dependencies

After the automated migration, run a full build and check for compilation errors. Most will be third-party libraries that need version bumps or replacements.

A systematic approach:

  1. Run mvn dependency:tree and search the output for any artifact still pulling in javax.* APIs.
  2. Check each offending dependency for a Jakarta-compatible version.
  3. If no compatible version exists, look for an alternative library.
# Find dependencies still pulling in javax APIs
mvn dependency:tree | grep -i "javax\.\(servlet\|persistence\|validation\|annotation\)"

Common replacements:

Old DependencyJakarta Replacement
springfox-swagger2springdoc-openapi-starter-webmvc-ui 2.x
javax.activation:activationjakarta.activation:jakarta.activation-api
javax.xml.bind:jaxb-apijakarta.xml.bind:jakarta.xml.bind-api
com.sun.xml.bind:jaxb-implorg.glassfish.jaxb:jaxb-runtime 4.x
javax.mail:javax.mail-apijakarta.mail:jakarta.mail-api
javax.annotation:javax.annotation-apijakarta.annotation:jakarta.annotation-api

Step 7: Fix Tests

Tests break in two common ways during this migration.

First, test utilities that reference javax types directly:

// Before
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@DataJpaTest
class OrderRepositoryTest {
    @PersistenceContext
    private EntityManager entityManager;
}

// After
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;

@DataJpaTest
class OrderRepositoryTest {
    @PersistenceContext
    private EntityManager entityManager;
}

Second, mock setups that cast to javax types:

// Before - this will fail silently or throw ClassCastException
MockHttpServletRequest request = new MockHttpServletRequest();
// MockHttpServletRequest now implements jakarta.servlet.http.HttpServletRequest

// If you have custom filters casting to javax.servlet types, update them

Run your entire test suite. Don’t skip integration tests. The import-level changes are caught at compile time, but classpath conflicts and reflection-based lookups only surface when you actually run the code.

Common Pitfalls

Pitfall 1: Mixed javax and jakarta on the Classpath

This is the most common production issue. Your application starts, handles a few requests fine, then throws a confusing ClassCastException because two versions of the Servlet API are on the classpath. One dependency transitively pulled in javax.servlet-api while Spring Boot 3 provides jakarta.servlet-api.

The fix: explicitly exclude the old dependency.

<dependency>
    <groupId>some.legacy</groupId>
    <artifactId>legacy-library</artifactId>
    <version>1.2.3</version>
    <exclusions>
        <exclusion>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </exclusion>
    </exclusions>
</dependency>

Pitfall 2: Spring Security Changes That Look Like Jakarta Issues

Spring Boot 3 also upgraded Spring Security to 6.x, which introduced its own breaking changes. The WebSecurityConfigurerAdapter was removed. The authorizeRequests() method was replaced by authorizeHttpRequests(). These are Spring Security changes, not Jakarta changes, but they happen at the same time and people conflate them.

Before:

// This class no longer exists in Spring Security 6
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/api/**").authenticated();
    }
}

After:

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/**").authenticated()
        );
        return http.build();
    }
}

If you’re troubleshooting your migration and the error is about WebSecurityConfigurerAdapter or antMatchers, that’s a Spring Security 6 issue, not a Jakarta namespace issue.

Pitfall 3: Hibernate Upgrades Beyond the Namespace

Spring Boot 3 ships with Hibernate 6, which has its own set of changes beyond the namespace. The @Type annotation works differently. The ImplicitNamingStrategy changed. Some HQL syntax was tightened. If your entities used Hibernate-specific annotations heavily, expect some adjustment beyond just swapping javax.persistence for jakarta.persistence.

Pitfall 4: Properties Files Changed Too

Spring Boot 3 renamed some configuration properties. This is easy to miss because property files don’t give you compile-time errors.

# Before (Spring Boot 2.x)
spring.redis.host=localhost
spring.redis.port=6379

# After (Spring Boot 3.x)
spring.data.redis.host=localhost
spring.data.redis.port=6379

OpenRewrite catches these, but if you migrated manually, run your application with --debug logging to catch any unrecognized property warnings.

Testing Your Migration

A successful build is not a successful migration. You need a testing strategy that covers the areas where namespace changes cause runtime problems.

Compilation: The first gate. Run a clean build with no cached classes: mvn clean compile.

Unit tests: Should catch most import-level issues. Run the full suite: mvn test.

Integration tests: Critical. These exercise the actual dependency injection, database connections, and HTTP handling where classpath conflicts surface. Run mvn verify.

Smoke tests in a staging environment: Deploy the migrated application and exercise the main user flows. Pay special attention to:

  • Login and authentication (Spring Security 6 changes)
  • Database operations (Hibernate 6 behavior changes)
  • File uploads and downloads (Servlet API changes)
  • Any endpoint that uses Bean Validation (@Valid, @NotNull)
  • Scheduled tasks and async processing

If you have a comprehensive integration test suite, most issues will surface before deployment. If you don’t, the migration is a strong argument for building one.

Migration Checklist

Use this as a reference. Work through it top to bottom.

  1. Upgrade to Java 17 or later
  2. Move to Spring Boot 2.7.x latest and fix all deprecation warnings
  3. Run mvn dependency:tree and catalog all javax.* transitive dependencies
  4. Update pom.xml (or build.gradle) to Spring Boot 3.x parent
  5. Run OpenRewrite UpgradeSpringBoot_3_4 recipe
  6. Review the OpenRewrite diff for correctness
  7. Replace incompatible third-party libraries (SpringFox, old JAXB, etc.)
  8. Exclude any transitive javax.* API jars that conflict with jakarta.*
  9. Update persistence.xml, web.xml, and other XML descriptors
  10. Fix Spring Security 6 configuration changes
  11. Run mvn clean verify and fix all test failures
  12. Deploy to staging and run smoke tests
  13. Monitor logs for ClassNotFoundException or NoClassDefFoundError on javax.* types

When to Get Help

If your application is relatively simple — a few dozen entities, standard Spring Security, no exotic third-party integrations — you can probably handle this migration in a day or two with OpenRewrite doing the heavy lifting.

But if you’re dealing with a large codebase, custom Hibernate types, legacy libraries without Jakarta support, or multiple interconnected applications that need to migrate together, the complexity scales fast. The namespace change is mechanical, but auditing a dependency tree with hundreds of transitive dependencies and verifying runtime behavior across every integration point takes experience and systematic testing.

We do this work regularly. If your team is staring at a migration that’s bigger than a weekend project, reach out and we can walk through your specific situation.

Frequently Asked Questions

Why did javax change to jakarta in Spring Boot 3?

When Oracle transferred Java EE to the Eclipse Foundation, Oracle retained the rights to the javax namespace. The Eclipse Foundation had to rename everything to jakarta to continue developing the specifications independently. Spring Boot 3 adopted Jakarta EE 9+ as its baseline, which means all javax enterprise imports become jakarta imports.

Can I use javax and jakarta imports in the same project?

Not for the same specification. Spring Boot 3 requires jakarta namespace imports for all EE specifications like Servlet, JPA, Bean Validation, and CDI. However, some javax packages that are NOT part of Jakarta EE still use javax. For example, javax.sql, javax.crypto, and javax.swing are part of the core JDK and remain unchanged.

How long does a javax to jakarta migration take?

For a small to medium Spring Boot application, the namespace migration itself takes hours, not weeks, especially with OpenRewrite automation. The real time sink is third-party libraries that still depend on javax. Auditing and replacing those dependencies is what stretches a migration from a day to a few weeks, depending on the size of your dependency tree.

Does OpenRewrite handle the entire javax to jakarta migration automatically?

OpenRewrite handles the bulk of the work: renaming imports, updating annotations, adjusting XML configuration files, and migrating pom.xml dependencies. It does not fix third-party libraries that internally depend on javax, nor does it resolve runtime-only issues like reflection-based code that references javax class names as strings. You still need manual review and testing.

Java Modernization Readiness Assessment

15 questions your team should answer before starting a migration. Takes 10 minutes. Could save you months.