One API, Two Runtimes

Splitting the addon into a shared API module and two implementation modules — CDI SE and Quarkus/ArC — with full build quality enforcement.

3 modules 128 tests RAT + Checkstyle + Enforcer April 2026

Why Modularize?

After porting the addon to Quarkus/ArC, we had two working implementations — but they duplicated the @TestBean and @TestBeans annotations plus 38 test use-case classes. The Quarkus module also lacked the build quality plugins (RAT license checks, Checkstyle, Enforcer) that the CDI module enforced. Time to fix both.

The Target Structure

dynamic-cdi-test-bean-addon/          (parent POM, packaging=pom)
  checkstyle.xml                      (shared at root)
  api/                                (shared annotations + SPI + usecase test-jar)
  cdi-module/                         (CDI SE implementation)
  quarkus-module/                     (ArC implementation)

api

@EnableTestBeans, @TestBean, @TestBeans, the TestBeanContainerManager SPI, and a test-jar with 38 use-case classes reused by both implementations.

cdi

CDI SE implementation using the standard portable extension SPI. Registers as a TestBeanContainerManager via ServiceLoader. Profiles for Weld and OpenWebBeans.

quarkus

Quarkus/ArC implementation using BeanProcessor + BeanRegistrar SPIs. Registers as a TestBeanContainerManager via ServiceLoader.

The Key Design Decision

@EnableTestBeans lives in the API module alongside @TestBean and @TestBeans. A thin DelegatingJUnitExtension discovers the actual container manager via ServiceLoader. Each implementation module registers a TestBeanContainerManager provider.

Users always import the same annotation — org.os890.cdi.addon.dynamictestbean.EnableTestBeans — regardless of whether they use CDI SE or Quarkus. Switching runtimes requires only a Maven dependency change. Zero import changes.

Shared Test Use-Cases via test-jar

Both modules need the same 38 test classes (beans like GreetingConsumer, interfaces like Greeting, qualifiers like @Premium). Instead of duplicating them or creating a fourth module, we use Maven's test-jar goal:

<!-- api/pom.xml -->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <executions>
        <execution>
            <goals><goal>test-jar</goal></goals>
        </execution>
    </executions>
</plugin>
<!-- cdi-module/pom.xml and quarkus-module/pom.xml -->
<dependency>
    <groupId>org.os890.cdi.addon</groupId>
    <artifactId>dynamic-cdi-test-bean-addon-api</artifactId>
    <type>test-jar</type>
    <scope>test</scope>
</dependency>

Build Quality: Enforced Everywhere

The parent POM defines all quality plugins in <pluginManagement> and activates them globally. Every module gets:

Quarkus enforcer exception: ArC's 999-SNAPSHOT transitive dependencies have version mismatches (smallrye-common, gizmo2). The Quarkus module overrides the enforcer to skip dependencyConvergence while keeping all other rules.

What Changed in the Quarkus Module

The Quarkus module's class discovery had to learn a new trick. When use-case classes moved from target/test-classes/ (filesystem) to the API test-jar (classpath JAR), the directory scanner couldn't find them. The fix: scan both the filesystem and classpath JARs containing org/os890/cdi/addon/dynamictestbean/usecase/.

// Scan classpath JARs (e.g., API test-jar with usecase classes)
Enumeration<URL> resources = cl.getResources(
        "org/os890/cdi/addon/dynamictestbean/usecase/");
while (resources.hasMoreElements()) {
    URL resUrl = resources.nextElement();
    if ("jar".equals(resUrl.getProtocol())) {
        scanJar(resUrl, cl, classes);
    }
}

The Result

128
tests across 3 modules
API: 0 (test-jar only) · CDI: 64 · Quarkus: 64
ModuleTestsRATCheckstyleEnforcer
api0 (test-jar)0 unapproved0 violationspass
cdi640 unapproved0 violationspass
quarkus640 unapproved0 violationspass
BUILD SUCCESS. All three modules compile, pass quality checks, and run their full test suites with zero failures from a single mvn clean verify at the project root.

For Consumers

Pick the implementation that matches your runtime:

<!-- CDI SE (Weld / OpenWebBeans) -->
<dependency>
    <groupId>org.os890.cdi.addon</groupId>
    <artifactId>dynamic-cdi-test-bean-addon-cdi</artifactId>
    <scope>test</scope>
</dependency>

<!-- Quarkus / ArC -->
<dependency>
    <groupId>org.os890.cdi.addon</groupId>
    <artifactId>dynamic-cdi-test-bean-addon-quarkus</artifactId>
    <scope>test</scope>
</dependency>

The API module (@EnableTestBeans, @TestBean, @TestBeans) is pulled in transitively. Same annotations, same imports, same test patterns. Switching runtimes is a Maven dependency change — zero import changes in test classes.

← Quarkus Port next post →