Skip to content
Jakarta EE — Explained with Examples

Jakarta EE — Explained with Examples

DodaTech Updated Jun 15, 2026 6 min read

Jakarta EE (formerly Java EE) is a set of enterprise Java specifications for building large-scale, distributed, and transactional applications, providing standardized APIs for web services, persistence, messaging, and dependency injection.

Why Jakarta EE Matters

Jakarta EE is the open-source evolution of Java EE, governed by the Eclipse Foundation. It powers critical enterprise systems in banking, insurance, telecom, and government. Unlike Spring (a framework), Jakarta EE is a standard — your skills transfer across any compliant implementation (Payara, WildFly, TomEE, GlassFish). DodaTech uses Jakarta EE for legacy system integration and batch processing pipelines.

Jakarta EE vs Spring Boot

FeatureJakarta EESpring Boot
PhilosophyStandardized platformFramework opinionated
ServerRequires app server (Payara, WildFly)Embedded server (Tomcat, Jetty)
DICDI (Contexts and Dependency Injection)Spring IoC
PersistenceJPA (Hibernate)Spring Data JPA
RESTJAX-RS (Jersey, RESTEasy)Spring MVC
Startup timeSlower (app server)Faster (embedded)
WeightHeavier (full app server)Lighter (only needed starters)

Jakarta EE Architecture

    graph TD
    subgraph Client[<b>Client Tier</b>]
        Browser[Browser / Mobile]
        Other[Other Services]
    end
    subgraph Web[<b>Web Tier</b>]
        Servlet[Servlets<br/>HTTP handling]
        JAXRS[JAX-RS<br/>REST endpoints]
        JSP[JSP / Facelets<br/>Views]
    end
    subgraph Business[<b>Business Tier</b>]
        EJB[EJB<br/>Business logic]
        CDI[CDI<br/>Dependency Injection]
        BeanValidation[Bean Validation]
    end
    subgraph Data[<b>Data Tier</b>]
        JPA[JPA<br/>Object-relational mapping]
        JTA[JTA<br/>Transactions]
        JMS[JMS<br/>Messaging]
    end
    Client --> Servlet
    Client --> JAXRS
    Servlet --> EJB
    JAXRS --> CDI
    EJB --> JPA
    EJB --> JTA
    EJB --> JMS
    style Jakarta fill:#f59e0b,color:#fff
  

Servlet — The Foundation

Servlets handle HTTP requests and responses. They are the lowest-level web API in Jakarta EE.

import jakarta.servlet.*;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;
import java.io.*;

@WebServlet("/api/hello")
public class HelloServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println("HelloServlet initialized");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        resp.setContentType("application/json");
        resp.setCharacterEncoding("UTF-8");

        String name = req.getParameter("name");
        String greeting = "Hello, " + (name != null ? name : "World") + "!";

        try (PrintWriter out = resp.getWriter()) {
            out.write("{\"message\": \"" + greeting + "\"}");
        }
    }

    @Override
    public void destroy() {
        System.out.println("HelloServlet destroyed");
    }
}

Expected output when hitting GET /api/hello?name=Jakarta:

{"message": "Hello, Jakarta!"}

JAX-RS — Building REST APIs

JAX-RS (JSR 370) provides annotations for RESTful web services. It’s the Jakarta EE equivalent of Spring MVC.

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import jakarta.inject.Inject;
import java.util.*;

@Path("/api/v1/devices")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class DeviceResource {

    @Inject
    private DeviceService deviceService;

    @GET
    public Response getAllDevices(
            @QueryParam("page") @DefaultValue("0") int page,
            @QueryParam("size") @DefaultValue("20") int size) {

        List<DeviceDTO> devices = deviceService.findAll(page, size);
        return Response.ok(devices).build();
    }

    @GET
    @Path("/{id}")
    public Response getDevice(@PathParam("id") Long id) {
        return deviceService.findById(id)
                .map(dto -> Response.ok(dto).build())
                .orElse(Response.status(Response.Status.NOT_FOUND).build());
    }

    @POST
    public Response createDevice(DeviceDTO device) {
        DeviceDTO created = deviceService.create(device);
        return Response.status(Response.Status.CREATED)
                .entity(created)
                .build();
    }

    @PUT
    @Path("/{id}")
    public Response updateDevice(@PathParam("id") Long id, DeviceDTO device) {
        DeviceDTO updated = deviceService.update(id, device);
        return Response.ok(updated).build();
    }

    @DELETE
    @Path("/{id}")
    public Response deleteDevice(@PathParam("id") Long id) {
        deviceService.delete(id);
        return Response.noContent().build();
    }
}

Deployment note: This resource must be registered in an Application subclass:

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("/api")
public class RestApplication extends Application {
    // JAX-RS automatically discovers @Path resources
}

JPA — Object-Relational Mapping

Jakarta Persistence (JPA) maps Java objects to database tables. It’s the same API that Spring Data JPA builds upon.

import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "devices")
@NamedQuery(name = "Device.findByType",
    query = "SELECT d FROM Device d WHERE d.type = :type")
public class Device {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    @NotBlank
    private String name;

    @Column(nullable = false)
    private String type;

    @Column(name = "signal_strength")
    private Integer signalStrength;

    @Column(nullable = false)
    private Boolean connected = false;

    @Version
    private Integer version;

    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;

    @PrePersist
    protected void onCreate() {
        createdAt = LocalDateTime.now();
    }

    // Getters and setters
}
import jakarta.ejb.Stateless;
import jakarta.persistence.*;
import java.util.*;

@Stateless
public class DeviceRepository {

    @PersistenceContext(unitName = "dodatechPU")
    private EntityManager em;

    public List<Device> findAll(int page, int size) {
        return em.createNamedQuery("Device.findAll", Device.class)
                .setFirstResult(page * size)
                .setMaxResults(size)
                .getResultList();
    }

    public Optional<Device> findById(Long id) {
        return Optional.ofNullable(em.find(Device.class, id));
    }

    public Device save(Device device) {
        if (device.getId() == null) {
            em.persist(device);
            return device;
        }
        return em.merge(device);
    }

    public void delete(Long id) {
        findById(id).ifPresent(em::remove);
    }

    public List<Device> findByType(String type) {
        return em.createNamedQuery("Device.findByType", Device.class)
                .setParameter("type", type)
                .getResultList();
    }
}

CDI — Contexts and Dependency Injection

CDI is the Jakarta EE dependency injection standard. It’s equivalent to Spring’s IoC container.

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.util.*;
import java.util.stream.*;

@ApplicationScoped
public class DeviceService {

    @Inject
    private DeviceRepository repository;

    @Inject
    private DeviceMapper mapper;

    public List<DeviceDTO> findAll(int page, int size) {
        return repository.findAll(page, size).stream()
                .map(mapper::toDTO)
                .collect(Collectors.toList());
    }

    public Optional<DeviceDTO> findById(Long id) {
        return repository.findById(id).map(mapper::toDTO);
    }

    @Transactional
    public DeviceDTO create(DeviceDTO dto) {
        Device device = mapper.toEntity(dto);
        return mapper.toDTO(repository.save(device));
    }

    @Transactional
    public DeviceDTO update(Long id, DeviceDTO dto) {
        Device existing = repository.findById(id)
                .orElseThrow(() -> new IllegalArgumentException("Device not found: " + id));

        existing.setName(dto.name());
        existing.setType(dto.type());
        existing.setSignalStrength(dto.signalStrength());
        existing.setConnected(dto.connected());

        return mapper.toDTO(repository.save(existing));
    }

    @Transactional
    public void delete(Long id) {
        repository.delete(id);
    }
}

EJB — Enterprise JavaBeans

EJBs handle transaction management, security, and concurrency declaratively. Modern Jakarta EE uses EJB Lite (annotations on POJOs).

import jakarta.ejb.*;
import java.util.concurrent.*;

@Singleton
@ConcurrencyManagement(ConcurrencyManagementType.CONTAINER)
public class DeviceCounter {

    private final AtomicLong totalDevices = new AtomicLong(0);

    @Lock(LockType.READ)
    public long getTotalDevices() {
        return totalDevices.get();
    }

    @Lock(LockType.WRITE)
    public void increment() {
        totalDevices.incrementAndGet();
    }

    @Schedule(hour = "0", minute = "0", second = "0")
    public void resetDaily() {
        totalDevices.set(0);
        System.out.println("Device counter reset for new day");
    }
}

Deployment — WildFly Example

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0"
    xmlns="https://jakarta.ee/xml/ns/persistence">
    <persistence-unit name="dodatechPU" transaction-type="JTA">
        <jta-data-source>java:/DodaTechDS</jta-data-source>
        <properties>
            <property name="jakarta.persistence.schema-generation.database.action"
                value="drop-and-create"/>
            <property name="hibernate.dialect"
                value="org.hibernate.dialect.PostgreSQLDialect"/>
            <property name="hibernate.show_sql" value="true"/>
        </properties>
    </persistence-unit>
</persistence>
# Deploy to WildFly
./standalone.sh -c standalone-full.xml
cp target/dodatech-api.war /path/to/wildfly/standalone/deployments/

# Or use the Maven plugin
mvn wildfly:deploy

Common Mistakes

  1. Confusing Jakarta EE with Spring APIs: Jakarta EE uses jakarta.* packages (not javax.* since Jakarta EE 9). Don’t mix them.
  2. Not configuring persistence.xml correctly: Missing persistence-unit or wrong transaction-type causes deployment failures.
  3. Forgetting to annotate @Stateless or @Singleton: EJBs need explicit annotations. A POJO without them gets no transaction or security management.
  4. Direct EntityManager injection in JAX-RS resources: Always inject via a service layer. Controllers shouldn’t know about persistence.
  5. Using EntityManager outside transactions: CRUD operations on EntityManager require an active transaction. Missing @Transactional causes TransactionRequiredException.

Practice Questions

  1. What is the difference between Jakarta EE and Java EE?
  2. How does CDI compare to Spring’s dependency injection?
  3. What annotations does JAX-RS use for HTTP methods?
  4. What is the purpose of @Version in JPA?
  5. How do EJBs handle concurrency?

Answers:

  1. Jakarta EE is the renamed, open-source evolution of Java EE after Oracle transferred it to the Eclipse Foundation. The package names changed from javax.* to jakarta.*.
  2. Both use annotations and interfaces for DI. CDI uses @Inject instead of Spring’s @Autowired. CDI is the standard; Spring’s DI is implementation-specific.
  3. @GET, @POST, @PUT, @DELETE, @PATCH. They map Java methods to HTTP methods on specific resource paths.
  4. @Version provides optimistic locking. Before updating, JPA checks that the version in the database matches the version read. If not, it throws OptimisticLockException.
  5. @Singleton EJBs default to container-managed concurrency. Add @Lock(LockType.READ) for read methods (allow concurrent access) and @Lock(LockType.WRITE) for write methods (exclusive access).

Mini Project: Jakarta EE REST API

Build a complete REST API for a device management system:

  1. Create a JPA @Entity for devices with fields: id, name, type, signal strength, connected, timestamps
  2. Implement a @Stateless EJB repository with CRUD operations
  3. Create a @Path("/devices") JAX-RS resource with full CRUD
  4. Add Bean Validation annotations to the entity
  5. Configure persistence.xml for PostgreSQL
  6. Deploy to WildFly or Payara

This mirrors how enterprise systems at DodaTech’s partners handle device inventory management in manufacturing environments.

Related topics: Java, Spring, JVM, API, OOP

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro