Skip to main content

Jakarta EE 11 for Spring Developers: What Actually Matters

If you write Spring for a living, “Jakarta EE 11” sounds like somebody else’s problem: application servers, EJBs, a different world. Yet every Spring Boot 4 application already runs on pieces of it — the servlet API under the web server, the persistence API under Hibernate, the validation API under @NotNull. When Jakarta EE 11 shipped, most of it arrived in your project without a headline. This article separates what actually changed in those pieces from what merely got a new number. It does that by reading the real API jars — EE 10 next to EE 11 — and Spring Boot’s own version list, rather than the marketing summary. You only need to know that Jakarta EE is a family of standard Java APIs and that Spring uses several of them.
Versions and status. Spring Boot 4.1.1 (Spring Framework 7.0.9, Tomcat 11.0.24, Hibernate 7.4.5), JDK 25.0.4, Maven 3.9. Jakarta jars compared: Servlet 6.0.0→6.1.0, Persistence 3.1.0→3.2.0, Validation 3.0.0→3.1.0, Annotations 2.1.1→3.0.0, Concurrency 3.0.0→3.1.1, plus Data 1.0.2. Companion code: spring-boot-demo/jakarta-ee-11.

Spring Boot picks the Jakarta versions for you

The first thing to understand is that you do not choose the Jakarta API versions in a Spring Boot project; Boot’s dependency list (its “BOM”) does. Reading that list for Boot 4.1.1 answers most “which EE level am I on?” questions in one screen. Servlet 6.1.0, Persistence 3.2.0, Validation 3.1.1 and Annotations 3.0.0 are all EE 11 generation. Tomcat 11.0.24 is the embedded servlet container, Hibernate 7.4.5 the persistence provider. The second half of the output is the surprise: five of the six artifacts checked appear nowhere in the list (Data, Concurrency, CDI, Security, Faces); the single 1 is the JAX-RS API.
# 01-boot-managed-jakarta-apis (spring-boot-dependencies 4.1.1)
hibernate-validator.version = 9.1.3.Final
hibernate.version = 7.4.5.Final
jakarta-activation.version = 2.1.4
jakarta-annotation.version = 3.0.0
jakarta-inject.version = 2.0.1
jakarta-jms.version = 3.1.0
jakarta-json-bind.version = 3.0.2
jakarta-json.version = 2.1.3
jakarta-mail.version = 2.1.5
jakarta-management.version = 1.1.4
jakarta-persistence.version = 3.2.0
jakarta-servlet-jsp-jstl.version = 3.0.2
jakarta-servlet.version = 6.1.0
jakarta-transaction.version = 2.0.1
jakarta-validation.version = 3.1.1
jakarta-websocket.version = 2.2.0
jakarta-ws-rs.version = 4.0.0
jakarta-xml-bind.version = 4.0.5
jakarta-xml-soap.version = 3.0.2
jakarta-xml-ws.version = 4.0.3
jetty.version = 12.1.12
spring-data-bom.version = 2026.0.1
spring-framework.version = 7.0.9
tomcat.version = 11.0.24
--- artifacts NOT in the BOM (0 = Spring Boot does not manage them)
  jakarta.data-api                     0
  jakarta.enterprise.concurrent-api    0
  jakarta.enterprise.cdi-api           0
  jakarta.security.enterprise-api      0
  jakarta.faces-api                    0
  jakarta.ws.rs-api                    1

Captured in 01-boot-managed-jakarta-apis.txt.

Read the zeros. A zero means Boot does not manage that artifact: no version is chosen for you and nothing brings it in. Jakarta Data and Jakarta Concurrency are in that group, and two later sections explain what that means in practice.
Going deeper: how the list was produced, and what it does not cover
The capture script downloads the published spring-boot-dependencies POM for 4.1.1 from Maven Central and prints the version properties that name Jakarta artifacts, Tomcat, Jetty, Hibernate and Spring Data. The second half greps the same POM for four artifact ids. Two things to keep in mind. The list is exact for 4.1.1 only; Boot 4.2 milestones may differ. And “not managed” is about the BOM; you can still add such a dependency yourself with an explicit version.

Servlet 6.1: the level you are on, proven by asking the server

The servlet API is how a web server hands a request to your code. Spring MVC sits on top of it and hides it, so it is easy to lose track of the version. The module has one endpoint that asks the running container which level it implements:
@RestController
class ServletLevelController {

    @GetMapping("/servlet-level")
    String level(HttpServletRequest request) {
        var ctx = request.getServletContext();
        return ctx.getMajorVersion() + "." + ctx.getMinorVersion() + " (container: " + ctx.getServerInfo() + ")";
    }
}

Source: ServletLevelController.java, lines 8–17.

# 05-servlet-level  (Spring Boot 4.1.1, embedded Tomcat, JDK 25)
GET /servlet-level -> 6.1 (container: Apache Tomcat/11.0.24)

Captured in 05-servlet-level.txt.

That output comes from a real Boot 4.1.1 application started in a test: Tomcat 11.0.24, Servlet 6.1. Comparing the 6.0.0 and 6.1.0 jars shows a small change — 26 members added, 3 removed, and one new class, HttpSession.Accessor. Servlet 6.1 is an incremental release; for a Spring MVC application there is nothing to migrate.
--- Servlet: jakarta.servlet-api 6.0.0 -> 6.1.0
  classes added   : 1   removed: 1
  members added   : 26   removed: 3
  + class jakarta.servlet.http.HttpSession$Accessor
  - class jakarta.servlet.http.Cookie$1

Captured in 02-api-diff.txt.

Going deeper: reading the comparison correctly
The comparison runs javap -public over every class in both jars and treats each signature as a string, so a changed signature counts as one removal and one addition. That is why “removed: 3” is not the same as “three methods deleted”. The one removed class, Cookie$1, is a compiler-generated inner class rather than API. I did not list the 26 added members individually; the counts are the claim. If you depend on a specific servlet feature, diff the two jars for that class yourself using the script.

Persistence 3.2: the release with real new API

This is the one EE 11 spec where the difference is large: 24 new classes and 251 members added against 39 removed (signature changes counted twice). It is also the only one you may touch directly, through the EntityManager. Hibernate 7 implements it, so you can use these methods today on Boot 4.1.1. The new classes are mostly options and configuration types: FindOption, LockOption, RefreshOption, PersistenceConfiguration, SchemaManager among them. They show up as extra varargs on existing operations:
# 03-entitymanager-3.2 (methods on jakarta.persistence.EntityManager that 3.1.0 did not have)
public abstract <C, T> T callWithConnection(ConnectionFunction<C, T>);
public abstract <C> void runWithConnection(ConnectionConsumer<C>);
public abstract <T> Query createNativeQuery(String, Class<T>);
public abstract <T> T find(Class<T>, Object, FindOption...);
public abstract <T> T find(EntityGraph<T>, Object, FindOption...);
public abstract <T> T getReference(T);
public abstract <T> TypedQuery<T> createQuery(TypedQueryReference<T>);
public abstract <T> TypedQuery<T> createQuery(criteria.CriteriaSelect<T>);
public abstract CacheRetrieveMode getCacheRetrieveMode();
public abstract CacheStoreMode getCacheStoreMode();
public abstract Query createQuery(criteria.CriteriaDelete<?>);
public abstract Query createQuery(criteria.CriteriaUpdate<?>);
public abstract StoredProcedureQuery createStoredProcedureQuery(String, Class<?>...);
public abstract void lock(Object, LockModeType, LockOption...);
public abstract void refresh(Object, RefreshOption...);
public abstract void setCacheRetrieveMode(CacheRetrieveMode);
public abstract void setCacheStoreMode(CacheStoreMode);

Captured in 03-entitymanager-3.2.txt.

Two groups matter most for everyday code. find(Class, Object, FindOption...), lock(..., LockOption...) and refresh(..., RefreshOption...) let you pass options in the call instead of through a hint map of strings. runWithConnection and callWithConnection hand you the JDBC connection the persistence context uses, in a callback, without unwrapping a Hibernate class.
Additions, not removals. The listing shows method signatures 3.1 did not have; several are new overloads of existing operations, and I did not test whether each old call still behaves identically. The larger point is that these are new methods on an interface that providers implement, which is why the Hibernate 8 article on this site finds its problem in Spring’s persistence-unit class, not in your code.
Going deeper: reading the class list
The class list in 02-api-diff.txt is cut to the first twelve names per spec by the capture script, so the 24 added classes are not all shown. The count is complete; the list is a sample. This article did not run any of the 3.2 methods against a database; the claim is what the jar declares.

Validation and Annotations: mostly a version bump, one removal

Validation went from 3.0.0 to 3.1.0 with nine new classes — every one a package-info — and a single member swapped. In practical terms: no new constraint annotations, nothing to change. Annotations 3.0.0 is the one with an actual removal: jakarta.annotation.ManagedBean is gone. If your code or a library still uses it, that is a compile error waiting for the day the jar is upgraded.
--- Validation: jakarta.validation-api 3.0.0 -> 3.1.0
  classes added   : 9   removed: 0
  members added   : 1   removed: 1
--- Annotations: jakarta.annotation-api 2.1.1 -> 3.0.0
  classes added   : 2   removed: 1
  members added   : 0   removed: 1
  - class jakarta.annotation.ManagedBean

Captured in 02-api-diff.txt.

Scope of this claim. The comparison covers public signatures only. It cannot see changed behaviour, such as a stricter default in the validation reference implementation. Boot 4.1.1 manages Hibernate Validator 9.1.3.Final, which is a separate release train.

Jakarta Data 1.0: a standard your Spring project does not use

Jakarta Data is the EE 11 answer to what Spring Data popularised: declare a repository interface, get an implementation. The 1.0.2 API defines its own repository types, similar in spirit but not the same interfaces:
--- jakarta.data-api 1.0.2: types in jakarta.data.repository
  BasicRepository
  By
  CrudRepository
  DataRepository
  Delete
  Find
  Insert
  OrderBy
  Param
  Query
  Repository
  Save
  Update

Captured in 04-data-and-concurrency.txt.

Two checks show where Spring stands. Spring Boot’s dependency list does not manage jakarta.data-api (section one, count zero). And the Spring Data jars in the same release train contain no jakarta.data classes at all:
# 06-spring-data-and-jakarta-data (classes mentioning jakarta/data inside Spring Data jars from Spring Data 2026.0.1)
  spring-data-commons 4.1.1: entries under jakarta/data/: 0; manifest lines mentioning jakarta.data: 0
  spring-data-jpa 4.1.1: entries under jakarta/data/: 0; manifest lines mentioning jakarta.data: 0

Captured in 06-spring-data-and-jakarta-data.txt.

The takeaway. On Spring Boot 4.1.1 you write Spring Data repositories, not Jakarta Data ones, and the two do not interoperate. Jakarta Data matters if you expect to move code between Spring and a Jakarta EE runtime, or read code that uses @Find and @Insert. It is not something to adopt in a Boot project today.
Going deeper: what this does and does not prove
The check shows the Spring Data 4.1.1 commons and JPA jars do not contain Jakarta Data classes and do not mention them in the manifest. It does not prove Spring will never support it, and I did not check other Spring Data modules.

Concurrency 3.1 and virtual threads: two different switches

Virtual threads are lightweight threads that make blocking code cheap. Two unrelated settings now mention them, and they are easy to confuse. The first is Spring Boot’s own: spring.threads.virtual.enabled. Boot ships it in its configuration metadata as a boolean, default false:
# 07-virtual-threads-property (spring-boot-autoconfigure 4.1.1 configuration metadata)
spring.threads.virtual.enabled (java.lang.Boolean, default False): Whether to use virtual threads.

Captured in 07-virtual-threads-property.txt.

The second is new in Jakarta Concurrency 3.1: the annotation that defines a managed executor gained a virtual attribute (and qualifiers). Comparing 3.0.0 and 3.1.1 shows exactly that difference:
--- jakarta.enterprise.concurrent-api 3.0.0: attributes of @ManagedExecutorDefinition
  String name();
  String context();
  long hungTaskThreshold();
  int maxAsync();
--- jakarta.enterprise.concurrent-api 3.1.1: attributes of @ManagedExecutorDefinition
  String name();
  Class<?>[] qualifiers();
  String context();
  long hungTaskThreshold();
  int maxAsync();
  boolean virtual();

Captured in 04-data-and-concurrency.txt.

Two switches, two ownersspring.threads.virtual.enabledSpring Boot property, default false@ManagedExecutorDefinition(virtual=true)Jakarta Concurrency 3.1, new attributeconcurrent-api: not in Boot 4.1.1 BOMSetting one does nothing to the other; the second only exists if you add the Concurrency API yourself.
The diagram shows the practical point: Boot users enable virtual threads with the Boot property; the Jakarta attribute belongs to executors defined through the Concurrency API, which Boot does not put on your classpath.
What I did not test. I did not run a workload with either switch, and I did not check which Boot components consult the property. The claims above are about what the artifacts declare, not about measured behaviour.

So what actually matters?

EE 11 specChange for a Spring Boot 4 projectAction
Servlet 6.1Incremental; Tomcat 11.0.24 implements it (proved by request)None
Persistence 3.2Real new API; options on find, lock, refresh, connection callbacksUse if useful; no migration
Validation 3.1No new API of substanceNone
Annotations 3.0ManagedBean removedGrep your code and libraries
Data 1.0Separate repository model; not used by Spring DataIgnore unless moving between runtimes
Concurrency 3.1virtual attribute; jar not managed by BootUse Boot’s property instead
Should you care about Jakarta EE 11? Only about one thing in the list: the Persistence 3.2 additions, which you can use today. Everything else is either invisible or something Spring Boot does not adopt. What this article cannot tell you is how a full Jakarta EE runtime behaves; it looked only at the APIs a Spring Boot project actually meets.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.