Legacy-architecture Jakarta EE ledger: EJB/CMT/JNDI/JMS on WildFly — MTA-issue-rich sample
Find a file
anaeem fe0de1d011
Some checks failed
build-image / build (push) Has been cancelled
CI: build with docker (the runner provides docker, not podman)
2026-06-15 15:17:13 +01:00
.gitea/workflows CI: build with docker (the runner provides docker, not podman) 2026-06-15 15:17:13 +01:00
db Initial commit: legacy-architecture Jakarta EE ledger (EJB/CMT/JNDI/JMS) for MTA 2026-06-11 08:39:29 +01:00
src/main Initial commit: legacy-architecture Jakarta EE ledger (EJB/CMT/JNDI/JMS) for MTA 2026-06-11 08:39:29 +01:00
wildfly Provision WildFly from pom: cloud-server + postgresql-driver, LedgerDS + LedgerEvents 2026-06-14 04:01:04 +01:00
.gitignore Initial commit: legacy-architecture Jakarta EE ledger (EJB/CMT/JNDI/JMS) for MTA 2026-06-11 08:39:29 +01:00
Containerfile Add Containerfile + Gitea build-and-push workflow 2026-06-15 15:14:15 +01:00
pom.xml Add ejb layer so @EJB injection + @MessageDriven MDB activate (cloud-server lacks ejb3) 2026-06-14 04:22:56 +01:00
README.md Initial commit: legacy-architecture Jakarta EE ledger (EJB/CMT/JNDI/JMS) for MTA 2026-06-11 08:39:29 +01:00

ledger-ejb

A deliberately old-school Jakarta EE sample application for a VM-to-OpenShift migration test fleet. It is the RED verdict app: hard to containerize naively and intentionally rich in the patterns the Migration Toolkit for Applications (MTA / Konveyor) flags as cloud-readiness issues.

Domain: a double-entry ledger. Accounts, journal entries, and journal lines, where every entry must balance (sum of debits == sum of credits), with that invariant enforced in the EJB tier.

Why Jakarta EE 10 (jakarta.*) and not javax.*? The architecture is legacy on purpose (EJBs, container-managed transactions, JNDI datasource lookups, JMS/MDB). But WildFly 30's default profile is Jakarta EE 10; the old javax.* EE8 mode is gone in 30. So the app uses jakarta.* APIs to actually deploy on stock WildFly 30, while keeping the legacy architecture that MTA still flags for cloud readiness. That is the point.

Module layout

ledger-ejb/
├── pom.xml                      # single WAR, finalName=ledger -> ledger.war
├── README.md
├── db/
│   └── init.sql                 # idempotent schema + seed (8 accounts, 7 entries)
└── src/main/
    ├── java/com/example/ledger/
    │   ├── domain/              # JPA entities
    │   │   ├── Account.java
    │   │   ├── JournalEntry.java
    │   │   └── JournalLine.java
    │   ├── ejb/                 # EJB tier (CMT, JNDI, @Singleton/@Stateful/@Stateless)
    │   │   ├── AccountServiceBean.java        # @Stateless
    │   │   ├── LedgerServiceBean.java         # @Stateless, posts + sends JMS
    │   │   ├── JournalEntryBuilderBean.java   # @Stateful conversational builder
    │   │   ├── StartupSeedCheckBean.java      # @Singleton @Startup seed check
    │   │   └── UnbalancedEntryException.java  # @ApplicationException(rollback=true)
    │   ├── jms/
    │   │   └── LedgerEventsMDB.java           # @MessageDriven on LedgerEvents
    │   └── web/                 # plain servlets + JSP (no Spring)
    │       ├── AccountsServlet.java
    │       ├── LedgerServlet.java
    │       ├── PostEntryServlet.java          # drives the @Stateful builder
    │       └── HealthServlet.java             # 200 JSON with DB ping
    ├── resources/META-INF/
    │   └── persistence.xml      # ledgerPU -> java:jboss/datasources/LedgerDS
    └── webapp/
        ├── index.jsp            # redirect to /accounts
        └── WEB-INF/
            ├── beans.xml
            ├── jboss-ejb3.xml   # vendor MDB binding
            └── jsp/*.jsp

Build

Java 17 toolchain (the project targets maven.compiler.release=17):

mvn -B -DskipTests package
# -> target/ledger.war

If no local Maven is present, build inside a container. Because of an SELinux bind-mount quirk on this host, copy the source in, build, and copy the artifact back out rather than bind-mounting the workspace:

podman run --rm --name ledger-build registry.access.redhat.com/ubi9/openjdk-17 \
  sleep infinity &
podman cp . ledger-build:/home/jboss/src
podman exec -w /home/jboss/src ledger-build mvn -B -DskipTests package
podman cp ledger-build:/home/jboss/src/target/ledger.war ./target/ledger.war
podman stop ledger-build

Deploy to WildFly 30

  1. Apply the standalone.xml datasource and JMS queue snippets below.
  2. Create the database and run the schema/seed:
    psql -h <host> -U ledger -d ledger -f db/init.sql
    
  3. Drop the WAR into the deployments folder:
    cp target/ledger.war $WILDFLY_HOME/standalone/deployments/
    
  4. Browse to http://localhost:8080/ledger/ (redirects to the accounts list).
    • /ledger/accounts — chart of accounts
    • /ledger/ledger — posted journal entries
    • /ledger/post — multi-step posting form (uses the @Stateful builder)
    • /ledger/health200 JSON with a live DB ping via the EJB

standalone.xml snippets

1. Datasource — java:jboss/datasources/LedgerDS

Note the plaintext password (ledger-db-pw) embedded in the config. This is the discovery secrets case the fleet is meant to surface — do not "fix" it.

Add inside <subsystem xmlns="urn:jboss:domain:datasources:7.0"> <datasources>:

<datasource jndi-name="java:jboss/datasources/LedgerDS"
            pool-name="LedgerDS" enabled="true" use-java-context="true">
    <connection-url>jdbc:postgresql://ledger-db:5432/ledger</connection-url>
    <driver>postgresql</driver>
    <pool>
        <min-pool-size>2</min-pool-size>
        <max-pool-size>20</max-pool-size>
    </pool>
    <security>
        <user-name>ledger</user-name>
        <!-- plaintext credential — the discovery secrets case -->
        <password>ledger-db-pw</password>
    </security>
</datasource>

And register the PostgreSQL JDBC driver in the same subsystem's <drivers>:

<driver name="postgresql" module="org.postgresql">
    <driver-class>org.postgresql.Driver</driver-class>
</driver>

(Install the driver as a JBoss module, e.g. $WILDFLY_HOME/modules/org/postgresql/main/ with postgresql-42.7.4.jar and a module.xml, or deploy the driver JAR directly.)

2. JMS queue — java:/jms/queue/LedgerEvents

Uses WildFly's embedded Artemis. Add inside <subsystem xmlns="urn:jboss:domain:messaging-activemq:..."> <server name="default">:

<jms-queue name="LedgerEvents"
           entries="java:/jms/queue/LedgerEvents java:jboss/exported/jms/queue/LedgerEvents"/>

The LedgerServiceBean sends a TextMessage to this queue on every successful posting (transactionally, inside the container-managed transaction), and the LedgerEventsMDB consumes and logs each event.

Seed summary

db/init.sql is idempotent and creates:

  • 8 accounts1000 Cash, 1100 Accounts Receivable, 1500 Equipment, 2000 Accounts Payable, 2100 Loans Payable, 3000 Owner Equity, 4000 Sales Revenue, 5000 Office Expense (ASSET / LIABILITY / EQUITY / REVENUE / EXPENSE).
  • 7 balanced journal entries — capital injection, equipment purchase, credit sale, customer payment, supplies on account, bank loan, and a multi-line supplier payment + bank fee. Every entry satisfies sum(debit) == sum(credit).

MTA-relevant anti-patterns deliberately present

  • EJB session beans@Stateless, @Stateful, @Singleton @Startup.
  • Container-managed transactions (CMT)@TransactionAttribute throughout, @ApplicationException(rollback=true).
  • JNDI datasource lookupjava:jboss/datasources/LedgerDS via persistence.xml; not a config/env-driven connection.
  • Plaintext datasource password in standalone.xml (ledger-db-pw) — the discovery secrets case.
  • JMS + MDB@MessageDriven consumer plus transactional JMS send tied to the embedded Artemis broker (broker coupling, server-managed delivery).
  • @Stateful conversational state parked in the HttpSession — sticky, instance-pinned state that breaks horizontal scaling.
  • @Startup @Singleton with a boot-time DB dependency — deploy-time coupling to the database / ordered startup.
  • Vendor-specific deployment descriptorjboss-ejb3.xml binding the MDB to the proprietary Artemis resource adapter.
  • Server-supplied (provided) EE runtime — the app expects a full Jakarta EE application server (WildFly), not an embeddable/self-contained runtime.