JavaSystem Design
June 6, 2026

Building Enterprise Java APIs with Jakarta EE and WildFly

Middleware exists to remove repeated infrastructure work from business applications.

Instead of every Java service inventing its own way to handle dependency injection, HTTP APIs, messaging, persistence, transactions, configuration, and deployment, enterprise middleware provides a shared runtime and a set of standardized APIs. Jakarta EE, historically known as Java EE, is one of the most important examples of this idea in the Java ecosystem.

WildFly is a practical runtime for that model. It gives developers an open source Jakarta EE application server while still keeping the application code based on standards that can move, with some dependency changes, to other compatible servers.

The Problem

Enterprise Java applications usually need more than classes and methods. A typical backend service may need to:

  • Expose REST endpoints.
  • Convert JSON payloads into Java objects.
  • Manage object lifecycles.
  • Connect to a relational database.
  • Handle transactions.
  • Send or consume messages.
  • Support WebSocket communication.
  • Separate application code from runtime configuration.

Without a standard, every team solves these concerns differently.

Application code
  |
  +-- REST handling
  +-- Dependency wiring
  +-- Database mapping
  +-- Transaction handling
  +-- Messaging setup
  +-- WebSocket lifecycle
  +-- Runtime configuration

Jakarta EE gives a common programming model for these concerns. WildFly provides the runtime implementation.

Core Idea

Jakarta EE is not just a library. It is a specification family for server-side enterprise Java. The specification defines APIs, expected behavior, and compatibility tests. Implementations such as WildFly, WebSphere, WebLogic, Payara, and GlassFish provide concrete runtimes.

The key design benefit is portability. Application code can target standardized APIs instead of relying completely on one vendor-specific programming model.

Jakarta EE also has profiles. The full profile contains the broad enterprise feature set. The web profile is a lighter subset for web-focused scenarios.

A simplified view looks like this:

Java application code
  |
  v
Jakarta EE APIs
  |
  v
Application server implementation
  |
  v
Database, broker, network, external systems

The application server handles many cross-cutting details so that the application can focus more on business behavior.

WildFly in Practice

WildFly is the upstream open source application server that came from the JBoss Application Server family. The commercially supported JBoss Enterprise Application Platform is built from the same component base, but stabilized for enterprise support and certification.

WildFly can run in two main modes:

  • Standalone mode, where one Java process runs one server instance.
  • Domain mode, where a fleet of instances can be centrally managed.

Domain mode should not be confused with clustering or high availability. Those are separate concerns. A set of standalone servers may be clustered, and a domain may manage instances that are not clustered.

A WildFly installation is commonly organized around a few important directories:

wildfly/
  bin/          Startup scripts and command-line utilities
  modules/      Server modules and core dependencies
  standalone/   Runtime root for standalone mode

For local development, the workflow is straightforward:

./bin/standalone.sh
mvn clean package
cp target/payment-api.war /path/to/wildfly/standalone/deployments/

This is not the only deployment option, but it shows the traditional application-server model: package an application artifact, place it in the server deployment area, and let the server manage the runtime.

Dependency Injection with CDI

Contexts and Dependency Injection, usually called CDI, is the standard way to wire objects in Jakarta EE applications.

Instead of manually constructing every dependency, you declare injectable beans and let the runtime compose them.

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class PaymentService {
    private final PaymentRepository repository;

    @Inject
    public PaymentService(PaymentRepository repository) {
        this.repository = repository;
    }

    public Payment create(Payment payment) {
        return repository.save(payment);
    }
}

CDI also defines scopes. A scope controls how long a bean instance should live.

Common examples include:

  • @ApplicationScoped, for one instance tied to the application lifecycle.
  • @RequestScoped, for one instance tied to an HTTP request.
  • @SessionScoped, for one instance tied to an HTTP session.
  • @TransactionScoped, for one instance tied to a transaction boundary.
  • @Dependent, where lifecycle follows the object that receives the dependency.

Lifecycle hooks are also available:

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class CacheWarmup {
    @PostConstruct
    void start() {
        System.out.println("Prepare cache");
    }

    @PreDestroy
    void stop() {
        System.out.println("Release cache resources");
    }
}

The practical value is simple: object creation, object lifecycle, and dependency wiring are handled consistently.

REST APIs with JAX-RS

Jakarta RESTful Web Services, commonly known as JAX-RS, provides a declarative way to expose HTTP APIs.

A minimal application activates a root path and exposes a resource class.

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

@ApplicationPath("rest")
public class RestApplication extends Application {
}

A resource class can then map paths, media types, and HTTP operations.

import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/payments")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class PaymentResource {
    @Inject
    PaymentService paymentService;

    @POST
    @Path("/create")
    public Payment create(Payment payment) {
        return paymentService.create(payment);
    }

    @GET
    @Path("/find/{id}")
    public Payment find(@PathParam("id") String id) {
        return paymentService.find(id);
    }
}

For common POJO-style objects, JSON binding can map request and response bodies to Java objects without complex manual parsing.

public class Payment {
    private String id;
    private String currency;
    private String sender;
    private String recipient;
    private double amount;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }
}

The important design rule is to keep REST classes focused on transport concerns. They should translate HTTP calls into application actions, not become the home for all business logic.

Real-Time Communication with WebSocket

REST is request-response oriented. WebSocket is different. It supports full-duplex communication between client and server.

In Jakarta EE, a WebSocket endpoint can be declared with annotations.

import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.ServerEndpoint;

@ServerEndpoint("/payment-events")
public class PaymentSocketEndpoint {
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("Client connected " + session.getId());
    }

    @OnMessage
    public String onMessage(String message) {
        System.out.println("Message received " + message);
        return message;
    }

    @OnClose
    public void onClose(Session session) {
        System.out.println("Client disconnected " + session.getId());
    }

    @OnError
    public void onError(Session session, Throwable error) {
        System.out.println("Socket error " + session.getId());
    }
}

WebSocket is useful when a web application needs server-to-client interaction without repeatedly polling an endpoint. Payment status updates, notifications, collaboration features, and dashboards can benefit from this style.

The warning is reliability. Network connections can disappear, especially with mobile clients or complex network infrastructure. Production code should handle reconnects, duplicate messages, lost messages, and backend exceptions deliberately.

Persistence with JPA

Jakarta Persistence, usually known as JPA, standardizes Object-Relational Mapping. It maps relational tables to Java objects and gives developers a standard way to persist, query, and load entities.

A simple entity can be modeled with annotations.

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "payments")
public class PaymentEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int databaseId;

    @Column(name = "payment_id")
    private String paymentId;

    @Column(name = "amount")
    private double amount;

    public String getPaymentId() {
        return paymentId;
    }

    public void setPaymentId(String paymentId) {
        this.paymentId = paymentId;
    }
}

A repository can use EntityManager to query and persist entities.

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import java.util.List;

public class PaymentRepository {
    @PersistenceContext(unitName = "paymentDatabase")
    private EntityManager entityManager;

    public List<PaymentEntity> findAll() {
        Query query = entityManager.createQuery("select p from PaymentEntity p");
        return query.getResultList();
    }

    public PaymentEntity save(PaymentEntity payment) {
        entityManager.persist(payment);
        return payment;
    }
}

The value of JPA is not only less code. It separates business code from database connection details. The application server can know how to connect to the database, while the application works through standardized persistence APIs.

Messaging

Enterprise systems often need asynchronous communication. Jakarta EE includes messaging APIs for interacting with brokers. In a server such as WildFly, broker connection details are normally configured in the application server.

The application code can refer to a JNDI name such as:

java:/ConnectionFactory

The server configuration then maps that logical name to host, port, credentials, and broker-specific details.

This separation is useful. Application code can depend on a stable logical resource name while operations teams can configure the real broker connection in the runtime.

Where Jakarta EE Needs Help

Jakarta EE is strong in enterprise stability, vendor ecosystem, operations, transactions, persistence, and standard APIs. It is especially valuable when teams need long-lived systems and commercial support.

But traditional Java EE has had gaps for modern cloud-native systems.

Common missing or externally supplied areas include:

  • Advanced observability, such as metrics, tracing, health checks, and readiness probes.
  • REST API contracts through OpenAPI.
  • Cloud-native fault tolerance patterns, such as retry, fallback, and circuit breaker.
  • Some modern security features, such as OpenID Connect and JSON Web Token support.
  • Faster evolution for new runtime expectations.

These gaps are one reason MicroProfile and newer frameworks became relevant.

Practical Workflow

  1. Start with the business capability.
  2. Expose transport concerns through JAX-RS resources.
  3. Put behavior in services, not in resource classes.
  4. Use CDI to wire dependencies.
  5. Model persistent data with JPA entities.
  6. Keep database connection details in runtime configuration.
  7. Use WebSocket only when full-duplex communication is really needed.
  8. Use messaging when the workflow can become asynchronous.
  9. Keep server configuration reproducible.
  10. Add external observability and API documentation where the platform does not provide enough.

Common Mistakes

A common mistake is putting too much logic into REST resource classes. That makes the transport layer hard to test and hard to reuse.

Another mistake is treating the application server as an excuse for unclear configuration. Thread pools, data sources, broker connections, and authentication providers should be documented and versioned as carefully as code.

A third mistake is assuming standard APIs remove the need to understand the runtime. Jakarta EE gives portability, but each server still has its own operational model.

A fourth mistake is ignoring network edge cases in WebSocket and server-sent communication. Real-time features must be designed for reconnects and partial failures.

Checklist

  • Runtime and API versions are clear.
  • The application server mode is selected intentionally.
  • CDI scopes match object lifecycles.
  • REST resources remain thin.
  • JSON payloads map cleanly to Java objects.
  • JPA entities do not contain accidental transport logic.
  • Data sources are configured outside business code.
  • Messaging resources use stable logical names.
  • WebSocket error handling is explicit.
  • Observability gaps are handled with suitable tools or extensions.

Conclusion

Jakarta EE and WildFly are still useful for enterprise Java systems because they provide stable standards for common server-side needs.

Use Jakarta EE when standardization, operational maturity, transactions, persistence, and long-term support matter. Keep business logic separate from transport and infrastructure details, and be honest about the areas where modern cloud-native concerns require MicroProfile, Quarkus, or additional tooling.

Share:

Comments0

Home Profile Menu Sidebar
Top