A Round-Robin Load Balancer using JBoss EAP 8.1

Besides hosting applications that serve business logic, JBoss EAP 8.1 can also provide load-balancing capabilities. This is especially useful when JBoss EAP is placed as a reverse proxy in front of other systems.

Conceptually, it would look something like the image below. To keep things simple, every application involved will be located on the same server:

Let’s start by editing JBoss EAP’s standalone.xml and adding two new hosts behind it

<?xml version="1.0" encoding="UTF-8"?>

<server xmlns="urn:jboss:domain:20.0">
    
    <profile>
        <subsystem xmlns="urn:jboss:domain:undertow:14.0"
                   default-virtual-host="default-host"
                   default-servlet-container="default"
                   default-server="default-server"
                   statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}"
                   default-security-domain="other">
            
            <byte-buffer-pool name="default"/>
            <buffer-cache name="default"/>
            
            <server name="default-server">
                <http-listener name="default" socket-binding="http" redirect-socket="https" enable-http2="true"/>
                <https-listener name="https" socket-binding="https" ssl-context="applicationSSC" enable-http2="true"/>
                
                <host name="default-host" alias="localhost">
                    <location name="/" handler="proxy"/>
                    <http-invoker http-authentication-factory="application-http-authentication"/>
                </host>
            </server>
            
            <servlet-container name="default">
                <jsp-config/>
                <websockets/>
            </servlet-container>
            
            <handlers>
                <reverse-proxy name="proxy" connection-idle-timeout="60">
                    <host name="backend-host1" outbound-socket-binding="backend-host1" scheme="http" instance-id="backend-host1" path="/"/>
                    <host name="backend-host2" outbound-socket-binding="backend-host2" scheme="http" instance-id="backend-host2" path="/"/>
                </reverse-proxy>
            </handlers>
            
            <application-security-domains>
                <application-security-domain name="other" security-domain="ApplicationDomain"/>
            </application-security-domains>
        </subsystem>
    </profile>
    
    <socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
        <outbound-socket-binding name="backend-host1">
            <remote-destination host="127.0.0.1" port="8081"/>
        </outbound-socket-binding>
        
        <outbound-socket-binding name="backend-host2">
            <remote-destination host="127.0.0.1" port="8082"/>
        </outbound-socket-binding>
    </socket-binding-group>
</server>

By default, the load balancing in JBoss EAP uses the round-robin strategy. This means the response alternates between each backend host with every request. For example,

$ curl -kv http://localhost:8080/
*   Trying [::1]:8080...
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: application/json
< content-length: 23
< Date: Mon, 18 Aug 2025 07:45:21 GMT
<
* Connection #0 to host localhost left intact
{"hello":"from host 1"}                                       

$ curl -kv http://localhost:8080/
*   Trying [::1]:8080...
*   Trying 127.0.0.1:8080...
* Connected to localhost (127.0.0.1) port 8080
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< transfer-encoding: chunked
< Content-Type: application/json
< Date: Mon, 18 Aug 2025 07:45:19 GMT
<
* Connection #0 to host localhost left intact
{"hello":"from host 2"}                                                                            ?

As shown above, the responses alternate between host 1 and host 2.

Conclusion
With just a few configuration changes, JBoss EAP 8.1 can be used as both a reverse proxy and a load balancer for HTTP requests. Its default round-robin strategy makes it straightforward to distribute traffic across multiple backends.

Increase JBoss EAP 8 Deployment Timeout

I ran into a situation where I had to deploy a huge WAR file, but it took so long that the deployment ended up timing out. It gives this error on the JBoss console,

12:00:05,689 INFO  [stdout] (ServerService Thread Pool -- 74) begin CustomContextListener.contextInitialized
12:05:05,563 ERROR [org.jboss.as.controller.management-operation] (DeploymentScanner-threads - 2) WFLYCTL0348: Timeout after [300] seconds waiting for service container stability. Operation will roll back. Step that first updated the service container was 'deploy' at address '[("deployment" => "jboss-eap-long-deployment-time.war")]'
12:05:05,878 WARN  [org.jboss.as.controller.management-operation] (DeploymentScanner-threads - 2) WFLYCTL0496: Thread dump:
*******************************************************************************
"Reference Handler" Id=9 RUNNABLE
        at java.base@21.0.3/java.lang.ref.Reference.waitForReferencePendingList(Native Method)
        at java.base@21.0.3/java.lang.ref.Reference.processPendingReferences(Reference.java:246)
        at java.base@21.0.3/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:208)

......

12:10:05,697 INFO  [org.wildfly.extension.undertow] (ServerService Thread Pool -- 74) WFLYUT0022: Unregistered web context: '/' from server 'default-server'
12:10:05,717 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-4) WFLYSRV0028: Stopped deployment jboss-eap-long-deployment-time.war (runtime-name: jboss-eap-long-deployment-time.war) in 299823ms

It looks like there’s a hard timeout limit of 300 seconds, or 5 minutes, for deployments. The solution is to increase this timeout limit. In my case, I increased it to 15 minutes by modifying the standalone.conf.bat file:

set "JAVA_OPTS=%JAVA_OPTS% -Djboss.as.management.blocking.timeout=900"

After applying this change, here’s the result of a successful deployment:

16:14:08,138 INFO  [stdout] (ServerService Thread Pool -- 74) begin CustomContextListener.contextInitialized
16:14:11,163 INFO  [org.wildfly.core.installationmanager] (MSC service thread 1-8) WFLYIM0023: Installation was provisioned using the following channel versions: '[ManifestVersion{channelId='org.jboss.eap.channels:eap-8.1', description='EAP 8.1 Beta', version='1.0.0.Beta-redhat-00023'}]'
16:20:48,140 INFO  [stdout] (ServerService Thread Pool -- 74) finish CustomContextListener.contextInitialized
.......

If you want to simulate a slow deployment on JBoss EAP, you can use the sample WAR file available on the below Github link,

https://github.com/edwin/jboss-eap-long-deployment-time

Creating a Query Timeout on JBoss EAP 8

Sometimes we want to limit the time needed for each queries, this is necessary to prevent our system from being slow and unresponsive due to slow queries.

For this sample, we are going to run a query on PostgreSQL that can simulate slowness

select pg_sleep(10);

And lets start with a pom file,

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>simulate-jboss-connection-timeout</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>


    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>10.0.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <warName>${project.name}</warName>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

And some Java files,

package com.edw.helper;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class DatabaseHelper {
    private DataSource ds = null;

    public DatabaseHelper() {
        try {
            Context initCxt = new InitialContext();
            ds = (DataSource) initCxt.lookup("java:/pgsql-ds");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Connection getConnection() throws SQLException {
        return ds.getConnection();
    }
}

package com.edw.controller;

import com.edw.helper.DatabaseHelper;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

@WebServlet(name = "IndexController", urlPatterns = "/")
public class IndexController extends HttpServlet {

    private static final long serialVersionUID = 1L;

    private DatabaseHelper dbHelper = new DatabaseHelper();

    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException {

        // create a 10 second query
        String sql = "select pg_sleep(10)";

        try (Connection conn = dbHelper.getConnection();
             PreparedStatement preparedStmt = conn.prepareStatement(sql)) {

            // do some query
            ResultSet resultSet = preparedStmt.executeQuery();

        } catch (Exception e) {
            e.printStackTrace(); // we should see some timeout exception here
        }

        response.setContentType("application/json");

        PrintWriter out = response.getWriter();
        out.println("{\"hello\":\"world\"}");
    }

}

Above code is using a Connection Pool called “java:/pgsql-ds” and would simulate a slowness when being executed,

$ time curl -kv http://127.0.0.1:8080/simulate-jboss-connection-timeout/
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET /simulate-jboss-connection-timeout/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: application/json;charset=ISO-8859-1
< Content-Length: 19
< Date: Mon, 16 Jun 2025 08:04:50 GMT
<
{"hello":"world"}
* Connection #0 to host 127.0.0.1 left intact
real    0m 10.17s
user    0m 0.01s
sys     0m 0.01s

Lets try giving a timeout to that Connection Pool,

And try to do another curl test

$ time curl -kv http://127.0.0.1:8080/simulate-jboss-connection-timeout/
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET /simulate-jboss-connection-timeout/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: application/json;charset=ISO-8859-1
< Content-Length: 19
< Date: Mon, 16 Jun 2025 08:05:26 GMT
<
{"hello":"world"}
* Connection #0 to host 127.0.0.1 left intact
real    0m 5.22s
user    0m 0.00s
sys     0m 0.01s

And we can see that theres an exception logged at our JBoss console,

15:11:42,531 ERROR [stderr] (default task-1) org.postgresql.util.PSQLException: ERROR: canceling statement due to user request
15:11:42,532 ERROR [stderr] (default task-1) 	at deployment.simulate-jboss-connection-timeout.war//com.edw.controller.IndexController.doGet(IndexController.java:43)

Complete code can be viewed on below Github repository

https://github.com/edwin/simulate-jboss-connection-timeout

Securely Storing Database Password on JBoss EAP 8

Typically, we store database passwords in plain text on JBoss EAP 8. However, this approach is not considered a best practice due to security concerns. Therefore, it is important to encrypt the password thru several ways of password encryption method. And on this article we’ll try to do encryption using the JBoss EAP’s credential-store.

First we need to create a credential store to be stored in JBoss EAP, with the name of “my_custom_store” and “longpassword” as its password which is located in the JBoss data directory.


$ jboss-cli.sh
You are disconnected at the moment. Type 'connect' to connect to the server or 'help' for the list of supported commands.

[disconnected /] connect

[standalone@localhost:9990 /] /subsystem=elytron/credential-store=my_custom_store:add(path="my_custom_store.jceks", relative-to=jboss.server.data.dir, credential-reference={clear-text=longpassword}, create=true)
{"outcome" => "success"}

Next is storing my database password there,

[standalone@localhost:9990 /]  /subsystem=elytron/credential-store=my_custom_store:add-alias(alias=db_password, secret-value=mysecuredatabasepassword)

And validate it,

[standalone@localhost:9990 /] /subsystem=elytron/credential-store=my_custom_store:read-aliases()
{
    "outcome" => "success",
    "result" => ["db_password"]
}

Next is injecting the value of our secure password from credential store into our database connection. This is happen in our standalone.xml file,

<datasource jndi-name="java:/my-db" pool-name="my-db">
	<connection-url>jdbc:mysql://localhost:3306/test_db</connection-url>
	<driver-class>com.mysql.cj.jdbc.Driver</driver-class>
	<driver>mysql</driver>
	<security>
		<user-name>root</user-name>
		<credential-reference store="my_custom_store" alias="db_password"/>
	</security>
</datasource>

A successful database connection can be tested thru JBoss EAP web console,

Multi-Tenancy System with JBoss EAP 8 Connection Pooling

Basically a multi-tenancy system is a condition where one single instance of a software is able to handle multple distinct customers with each having their own database. The concept is perhaps can be seen in below image,

We can start by uploading database driver that is required to connect to the existing database, for this sample we are using MySql Database

creating a Database connection on JBoss EAP Datasource,

choose MySql,

create JNDI name

and select driver,

put database credentials there,

test the connection,

and save it.

If we have lots of different database connection, we can define it on our JBoss EAP standalone.xml manually instead of putting them one by one from JBoss EAP console,

<datasource jndi-name="java:/db-one-ds" pool-name="db-one-ds">
    <connection-url>jdbc:mysql://localhost:3306/db-one</connection-url>
    <driver-class>com.mysql.cj.jdbc.Driver</driver-class>
    <driver>mysql-connector-j-9.3.0.jar</driver>
    <security>
        <user-name>aaaa</user-name>
        <password>bbbb</password>
    </security>
</datasource>

<datasource jndi-name="java:/db-two-ds" pool-name="db-two-ds">
    <connection-url>jdbc:mysql://localhost:3306/db-two</connection-url>
    <driver-class>com.mysql.cj.jdbc.Driver</driver-class>
    <driver>mysql-connector-j-9.3.0.jar</driver>
    <security>
        <user-name>aaaa</user-name>
        <password>bbbb</password>
    </security>
</datasource>

Next is to create a database connection in our Java file

package com.edw.helper;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

public class DatabaseHelper {

    private DataSource ds = null;

    public DatabaseHelper() {
    }

    public DatabaseHelper(String customerId) {
        try {
            Context initCxt = new InitialContext();
            ds = (DataSource) initCxt.lookup(String.format("java:/%s-ds", customerId));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Connection getConnection() throws SQLException {
        return ds.getConnection();
    }
}

and create a service class.

package com.edw.service;

import com.edw.helper.DatabaseHelper;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

public class UserService {

    private DatabaseHelper databaseHelper;

    public UserService() {
    }

    public List<Map> select(String customerId, Integer start, Integer limit) {

        List<Map> list = new ArrayList<>();

        databaseHelper = new DatabaseHelper(customerId);
        String sql = "select * from tb_testing order by id limit ?, ?";

        try (Connection conn = databaseHelper.getConnection();
             PreparedStatement preparedStmt = conn.prepareStatement(sql)) {

            preparedStmt.setInt(1, start);
            preparedStmt.setInt(2, limit);

            try (ResultSet resultSet = preparedStmt.executeQuery()) {
                while (resultSet.next()) {
                    Integer id = resultSet.getInt(1);
                    String username = resultSet.getString(2);

                    list.add(new HashMap() {{
                        put("id", id);
                        put("username", username);
                    }});
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return list;
    }

}

and our controller file,

package com.edw.controller;

import com.edw.service.UserService;

import com.google.gson.Gson;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;

@WebServlet(name = "IndexController", urlPatterns = "/index")
public class IndexController extends HttpServlet {

    private static final long serialVersionUID = 1L;

    private UserService userService = new UserService();

    @Override
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException {

        String customerId = request.getParameter("customerId");
        List<Map> result = userService.select(customerId, 0, 10);

        response.setContentType("application/json");

        PrintWriter out = response.getWriter();
        out.println(new Gson().toJson(result));
    }
}

with pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.edw</groupId>
    <artifactId>multitenancy-connection-pool-with-jboss-8</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>jakarta.platform</groupId>
            <artifactId>jakarta.jakartaee-web-api</artifactId>
            <version>10.0.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.13.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.4.0</version>
                <configuration>
                    <warName>${project.name}</warName>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

and finally we can test our application connectivity by using below command,

$ curl -kv http://127.0.0.1:8080/index?customerId=db-one
*   Trying 127.0.0.1:8080...
* Connected to 127.0.0.1 (127.0.0.1) port 8080
> GET /index?customerId=db-one HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Connection: keep-alive
< Content-Type: application/json;charset=ISO-8859-1
< Content-Length: 594
< Date: Sun, 25 May 2025 15:31:57 GMT
<
[{"id":1,"username":"username 001"}]
* Connection #0 to host 127.0.0.1 left intact

Sample for the code can be found on below Github link,

https://github.com/edwin/multitenancy-connection-pool-with-jboss-8