What is Jakarta EE β and Why Should a Spring Developer Care?
Jakarta EE is a collection of specifications β not a
library, not a framework, not a product. Each specification defines an API
(interfaces, annotations, contracts) that vendors implement. JPA defines
@Entity and EntityManager; Hibernate is one
implementation. Servlet defines HttpServletRequest; Tomcat is
one implementation. The spec itself ships no runnable code.
This distinction matters because you are already using Jakarta EE
specifications in every Spring Boot project. When you write
@Entity, @NotBlank, or
HttpServletRequest, those annotations come from Jakarta EE
specs (jakarta.persistence, jakarta.validation,
jakarta.servlet). Spring doesn't reinvent these β it
implements or builds on top of them. Understanding Jakarta EE is
understanding the foundations Spring sits on.
| Layer | Who provides it | Example |
|---|---|---|
| Specification (API) | Jakarta EE | jakarta.persistence.@Entity |
| Implementation | Hibernate, Tomcat, Weld⦠| Hibernate persists the entity to the DB |
| Abstraction on top | Spring / Spring Data | JpaRepository wraps EntityManager |
Spring Boot doesn't replace Jakarta EE specs β it packages implementations of them (Hibernate, Tomcat, Hibernate Validator) and adds its own abstraction layer on top. That's the whole model.
History β Java EE, the Oracle Handoff, and the javaxβjakarta Rename
/*
* 1999 J2EE 1.2 β first enterprise platform, Servlets + EJB + JMS
* 2006 Java EE 5 β annotations replace XML-heavy config; JPA introduced
* 2009 Java EE 6 β CDI introduced; EJB simplified; REST (JAX-RS)
* 2013 Java EE 7 β WebSocket, JSON-P, Batch
* 2017 Java EE 8 β HTTP/2, JSON-B, Security API
* Oracle donates the platform to the Eclipse Foundation
* β Project renamed to Jakarta EE
* 2019 Jakarta EE 8 β identical to Java EE 8, just new governance
* 2020 Jakarta EE 9 β THE BREAKING CHANGE: javax.* β jakarta.*
* 2022 Jakarta EE 10 β Java 11+, CDI Lite, Core Profile
* 2024 Jakarta EE 11 β Java 21+, Virtual Threads, JPA 3.2
*/
Oracle donated the Java EE codebase to the Eclipse Foundation in 2017,
but retained the trademark on the javax namespace. The
Eclipse Foundation couldn't add new APIs or fix existing ones under
javax.* without Oracle's explicit permission for each
change β a governance deadlock. The only way forward was renaming the
namespace to one the Eclipse Foundation actually owned:
jakarta.*.
This was a breaking change. Any code, library, or framework that
imported javax.servlet.*, javax.persistence.*,
javax.validation.* had to update its imports. Spring
Framework 6 / Spring Boot 3 made exactly this migration β which is
why all code in this Bible uses jakarta.* and why Boot 2.x
code with javax.* is a different, incompatible world.
// Before Jakarta EE 9 / Spring Boot 2.x
import javax.persistence.Entity;
import javax.validation.constraints.NotBlank;
import javax.servlet.http.HttpServletRequest;
// Jakarta EE 9+ / Spring Boot 3+
import jakarta.persistence.Entity;
import jakarta.validation.constraints.NotBlank;
import jakarta.servlet.http.HttpServletRequest;
What is a Bean?
The word "bean" is overloaded across Java's history and means different things in different contexts. Before conflating them, it helps to distinguish them clearly:
| Type | What it is | Who manages it | Key annotation |
|---|---|---|---|
| JavaBean | A plain Java class with a no-arg constructor, private fields, and public getters/setters β a convention, not a framework concept | Nobody β you manage it with new |
None |
| CDI Bean | Any class the CDI container discovers, instantiates, and wires β the Jakarta EE standard for managed objects | CDI container (Weld, OpenWebBeans) | @ApplicationScoped, @RequestScoped, etc. |
| Spring Bean | Any object managed by the Spring IoC container β same concept as CDI, different implementation | Spring ApplicationContext |
@Component, @Service, @Bean |
| EJB | A CDI bean with additional container-managed services: distributed transactions, security, async, timers | EJB container (part of Jakarta EE server) | @Stateless, @Stateful, @MessageDriven |
| JPA Entity | A class mapped to a database table β managed by the JPA persistence context, not the CDI or Spring container | JPA EntityManager |
@Entity |
What makes something a "bean" in any of these contexts is not having
an annotation β it's being managed by a container. The container
creates the instance, resolves and injects its dependencies, calls
lifecycle callbacks (@PostConstruct,
@PreDestroy), and destroys it when appropriate. The
critical implication: you don't use new to get a bean.
If you instantiate a CDI or Spring bean with new, you get
an object β but none of the container's services apply to it. No
injection, no transaction management, no interceptors.
// CDI bean β container creates, injects, and manages lifecycle
@ApplicationScoped
public class UserService {
@Inject
private UserRepository repository; // injected by container β never null after @PostConstruct
@PostConstruct // called AFTER injection is complete β safe to use injected fields here
public void init() {
log.info("UserService ready, repository={}", repository);
}
@PreDestroy // called before container destroys the instance β clean up resources
public void cleanup() {
// release resources if needed
}
}
// WRONG β this is just a regular object, no injection, no lifecycle
UserService svc = new UserService(); // repository is null β will NPE on first call
CDI Scopes β How Long a Bean Lives
@RequestScoped // one instance per HTTP request β destroyed when request ends
@SessionScoped // one instance per HTTP session β destroyed when session expires
@ApplicationScoped // one instance for the whole application β effectively a singleton
@ConversationScoped // developer-controlled scope β spans multiple requests explicitly
@Dependent // default: same scope as the bean that injects it (no shared instance)
When a wider-scoped bean (e.g. @ApplicationScoped)
injects a narrower-scoped bean (e.g. @RequestScoped),
CDI cannot inject the actual @RequestScoped instance β
it doesn't exist yet at injection time, and it changes per request.
Instead, CDI injects a client proxy: a generated
subclass that forwards every method call to the correct real instance
for the current context. This is transparent to your code, but it
imposes three requirements on any proxied class: it must
not be final, its methods must
not be final, and it must have a
no-arg constructor (can be package-private). Violating
any of these produces a CDI deployment error at startup, not a
runtime exception.
Jakarta EE Specifications You Already Use in Spring Boot
The following Jakarta EE specs are active in every standard Spring Boot 3.x project β not optionally, by default. This is the concrete answer to "why does this topic matter if I'm a Spring developer":
| Spec | Package | Used in Spring Boot via |
|---|---|---|
| Servlet 6.1 | jakarta.servlet.* |
Every HTTP request β DispatcherServlet is a HttpServlet |
| JPA 3.2 | jakarta.persistence.* |
@Entity, @Id, EntityManager β Hibernate implements it |
| Bean Validation 3.0 | jakarta.validation.* |
@NotBlank, @Email, @Valid β Hibernate Validator implements it |
| CDI 4.1 (partial) | jakarta.inject.* |
@Inject β Spring supports it as an alternative to @Autowired |
| Transactions (JTA) | jakarta.transaction.* |
@Transactional β Spring's transaction manager implements JTA semantics |
Quarkus (Red Hat's cloud-native framework) implements Jakarta EE specs
directly β CDI for injection, JAX-RS for REST, JPA for persistence β
without the Spring abstraction layer. If you encounter Quarkus in the
wild or in a job description, understanding this page is what makes
Quarkus's annotations readable: @Path is JAX-RS,
@ApplicationScoped is CDI, @Inject is
Jakarta Inject β all covered in the pages of this topic.
Application Servers vs Embedded Server β The Deployment Model Difference
/*
* JAKARTA EE (traditional) β external application server:
*
* [WildFly / Payara]
* βββ runs permanently
* βββ you deploy a WAR/EAR into it
* βββ server provides: CDI, JPA, Transactions, Security, JMS...
* βββ multiple apps can share one server
*
* SPRING BOOT β embedded server model:
*
* [Your JAR]
* βββ contains an embedded Tomcat/Jetty/Undertow
* βββ java -jar myapp.jar β app starts its own server
* βββ one app per JVM process (the 12-factor way)
* βββ no external server dependency at runtime
*/
| Aspect | Jakarta EE (external server) | Spring Boot (embedded) |
|---|---|---|
| Deployment artifact | WAR or EAR file | Executable JAR (fat jar) |
| Runtime | Deploy to running WildFly/Payara | java -jar app.jar β self-contained |
| Container lifecycle | Server manages everything | Spring manages its own context |
| Shared resources | Multiple apps share connection pools, JMS, etc. | Each app owns its resources |
| Cloud / containers | Possible but heavier | Native fit β one process, one container image |
| Typical use today | Legacy enterprise systems, banking, government | New microservices, cloud-native apps |
The "Jakarta EE requires a heavy application server" stereotype is outdated. Quarkus and Helidon both implement Jakarta EE specs and compile to lightweight executable JARs (Quarkus even supports GraalVM native image). The deployment model is now a choice, not a constraint of using Jakarta EE specs. What remains true is that traditional projects using WildFly or Payara still follow the WAR/EAR model β that's what you'll encounter in legacy enterprise systems.
JSON in Jakarta EE β JSON-P, JSON-B, and Why Jackson Won
Jakarta EE defines two specs for JSON:
| Spec | Package | Level | Equivalent in Jackson |
|---|---|---|---|
| JSON-P (Processing) | jakarta.json.* |
Low-level: manual parsing and building via JsonObject, JsonParser |
Jackson's JsonParser / JsonGenerator streaming API |
| JSON-B (Binding) | jakarta.json.bind.* |
High-level: object β JSON via Jsonb, mirrors Jackson's ObjectMapper |
Jackson's ObjectMapper |
Both are valid specs, correctly designed, and implemented by libraries like
Yasson (JSON-B) and Eclipse Parsson (JSON-P). They lost to Jackson in
practice for three reasons: Jackson predated both specs and was already
entrenched; Jackson's annotation model (@JsonProperty,
@JsonIgnore) is richer and more flexible; and Spring Boot
chose Jackson as its default HttpMessageConverter, which
gave it the ecosystem momentum that compounds over time.
JSON-P's JsonParser is a true streaming parser β it reads
a JSON document token by token without loading it into memory. For
files measured in hundreds of MB or GB (log exports, data dumps),
the streaming API avoids an OutOfMemoryError that
Jackson's tree model would cause. Jackson also has a streaming API
(JsonParser), but JSON-P's is the Jakarta standard and
is supported natively on any Jakarta EE server without adding a
dependency. For everything else in a Spring Boot project, Jackson is
the right tool β covered in depth in
Jackson β JSON in Java.
Jakarta EE Profiles β Full, Web, and Core
Jakarta EE is not monolithic β you don't have to use all specs. Profiles define which subset is required for certification:
| Profile | Includes | Typical use | Example server |
|---|---|---|---|
| Core Profile | CDI Lite, JSON-B, JSON-P, REST Client (JAX-RS client) | Microservices, cloud-native, serverless | Helidon, Open Liberty Micro |
| Web Profile | Core + Servlets, JPA, Bean Validation, EJB Lite, WebSocket | Web applications, REST APIs β most real projects fit here | TomEE, Payara Micro |
| Full Platform | Web + JMS, full EJB, JAXB, Batch, Concurrency | Large enterprise monoliths with messaging and batch processing | WildFly, Payara Server |
<!-- Full Platform β compile against everything, server provides at runtime -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>11.0.0</version>
<scope>provided</scope> <!-- server provides at runtime -->
</dependency>
<!-- Web Profile only β smaller compile-time surface -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>11.0.0</version>
<scope>provided</scope>
</dependency>
The provided scope is critical β these APIs are provided
by the application server at runtime. Including them with
compile scope would package them into your WAR and cause
class conflicts with what the server already ships.
Interview Questions
Q: What is the difference between a specification and an implementation in the context of Jakarta EE?
A specification defines the API β interfaces, annotations, and contracts β
without providing runnable code. An implementation is the library or server
that actually executes against that API. JPA is a specification;
Hibernate is its most common implementation. Servlet is a specification;
Tomcat is its most common implementation.
Q: Why did Jakarta EE rename from javax.* to jakarta.* β what forced it?
Oracle retained the trademark on the javax namespace after
donating the platform to the Eclipse Foundation. The Foundation could not
evolve the APIs under javax.* without Oracle's explicit
approval per change. To regain control of the platform's evolution,
the namespace was renamed to jakarta.* β a breaking change
that required every library, framework, and application to update its
imports.
Q: What is a CDI Bean and how does it differ from a plain Java object?
A CDI bean is a Java object whose lifecycle is managed by the CDI
container. The container creates it, injects its dependencies, calls
@PostConstruct after injection is complete, and calls
@PreDestroy before destroying it. A plain object created with
new gets none of this β injection fields stay null, no
lifecycle callbacks are invoked, and no interceptors apply.
Q: A Spring Boot developer says "I don't need to know Jakarta EE." What's the precise technical counter-argument?
Every Spring Boot 3.x project uses Jakarta EE specs directly:
jakarta.servlet.* for HTTP processing,
jakarta.persistence.* for JPA entities,
jakarta.validation.* for Bean Validation, and
jakarta.transaction.* for transaction semantics.
Spring doesn't replace these specs β it implements them or builds
abstractions on top. Not understanding the spec/implementation boundary
means not understanding what Spring Data JPA actually does (wraps
EntityManager), why @Transactional works the
way it does (JTA semantics), or why Quarkus annotations look familiar
(it implements the same specs directly).
Q: Why does CDI inject a proxy instead of the real bean, and what constraints does that impose?
When a longer-lived bean (e.g. @ApplicationScoped) receives
an injection of a shorter-lived bean (e.g. @RequestScoped),
the real instance doesn't exist at injection time β it changes per request.
CDI solves this with a generated client proxy: a subclass that intercepts
every method call and forwards it to the correct real instance for the
current context. Because it subclasses the bean, three constraints apply:
the class cannot be final, none of its methods can be
final, and it must have a no-arg constructor. A violation
is a deployment error at startup, not a runtime NullPointerException.
Q: What is the difference between the Web Profile and the Core Profile, and when would you choose each?
The Core Profile (introduced in Jakarta EE 10) is the minimal subset β
CDI Lite, JSON-B, JSON-P, and the JAX-RS client β designed for
microservices that need to be small and start fast, targeting Quarkus and
Helidon. The Web Profile adds Servlets, full JPA, Bean Validation, EJB
Lite, and WebSocket β everything needed for a complete web application.
Choose Core when building a stateless service or function that runs on
a reactive runtime; choose Web Profile for anything that needs HTTP
request handling, database persistence, or session management.