Quarkus Development Skill
Expert guidance for Quarkus framework and application development.
Build Commands
Prefer mvnd (Maven Daemon) over ./mvnw when available — it keeps a
warm JVM across builds and parallelizes modules by default.
Fall back to ./mvnw if mvnd is not installed.
mvnd -Dquickly # Full build, skip tests/docs/native (parallel by default)
mvnd install -f extensions/<name>/ # Build one extension
mvnd verify -f extensions/<name>/ -Dtest-containers -Dstart-containers # Run extension tests
mvnd test -Dtest=MyTest -f extensions/<name>/deployment/ # Run single test
- Always use
install(not justcompile) — downstream modules need the jar in the local repo. - If you change a runtime module, rebuild its deployment module too.
- Always add
-Dtest-containers -Dstart-containerswhen running tests. - Podman is available as a Docker-compatible container engine. Tests
that use Testcontainers work transparently with podman — do not skip
container-based tests because the
dockerCLI is unavailable. - Do not use
-Dno-format— formatting and import sorting are applied automatically during compilation. - When using
./mvnwinstead ofmvnd, add-T 0.5Cto parallelize module builds. - Remember the build is very long (10+ minutes). It is not a viable strategy to re-run it just to look more precisely for errors. If you intend to do that, make sure to save build logs to a file when building, then work on that file for various grep operations.
Project Structure
extensions/<name>/runtime/— Runtime classes, recorders, beansextensions/<name>/deployment/—@BuildStepprocessors (tests live here)extensions/<name>/deployment-spi/— Build items shared between extensionsextensions/<name>/runtime-dev/— Dev mode runtime classes
Deployment depends on runtime, NEVER the reverse. Runtime code must not reference deployment classes.
Build Steps
Recorders Bridge Deployment and Runtime
A @Recorder lives in the runtime module but is invoked from deployment
build steps. It generates bytecode that runs at application startup.
// In deployment module:
@BuildStep
@Record(ExecutionTime.RUNTIME_INIT)
void configure(MyRecorder recorder, ...) {
recorder.doSomething(buildTimeValue);
}
Build Items
SimpleBuildItem— at most one instance per buildMultiBuildItem— multiple instances collected asList<>- Build items in
deployment-spi/are shared between extensions - Build items in
deployment/are internal to the extension
Cycle Detection
The build step chain is validated statically. If step A produces item X, and step B consumes X and produces item Y, and Y feeds back to A's inputs, a cycle is detected — even if the production is conditional.
Common cycle pattern with BeanDiscoveryFinishedBuildItem:
BeanDiscoveryFinished → (your step) → AdditionalBeanBuildItem → Arc → BeanDiscoveryFinished
Fixes:
- Move
AdditionalBeanBuildItemproduction to a step that does not depend onBeanDiscoveryFinishedBuildItem - Convert from
AdditionalBeanBuildItemtoSyntheticBeanBuildItem(feeds into a later Arc phase, afterBeanDiscoveryFinished) - Extract the offending production into a separate build step
SyntheticBeanBuildItem does NOT cause cycles because it feeds into
BeanRegistrationPhaseBuildItem, which is after BeanDiscoveryFinished.
Synthetic Beans
syntheticBeans.produce(SyntheticBeanBuildItem.configure(MyBean.class)
.scope(Singleton.class)
.unremovable()
.setRuntimeInit()
.addInjectionPoint(ClassType.create(DotName.createSimple(MyDep.class)))
.createWith(recorder.myBeanSupplier())
.done());
- Use
.setRuntimeInit()if the bean needs runtime config - Declare all dependencies as
.addInjectionPoint()— Arc removes beans it considers unused, and programmatic lookups viaArc.container()are invisible to Arc's unused bean detection - Use
.unremovable()for beans looked up programmatically
Hibernate ORM
Programmatic Transactions
- Prefer
QuarkusTransactionoverUserTransactionQuarkusTransaction.requiringNew().run(() -> { ... })QuarkusTransaction.joiningExisting().run(() -> { ... })
Query APIs
- Prefer
session.createSelectionQuery()(Hibernate 6+ API) - Avoid legacy
createQuery()unless necessary
Session Management
@Inject Sessionfor regular Hibernate Session@Inject StatelessSessionfor stateless operations@Inject Mutiny.SessionFactoryfor Hibernate Reactive
Testing
Test Annotations
QuarkusExtensionTest(with@RegisterExtension) — For deployment module tests. Creates a synthetic application.@QuarkusTest— Full application startup. For integration tests.@QuarkusIntegrationTest— Tests against built artifact.
QuarkusExtensionTest Patterns
@RegisterExtension
static final QuarkusExtensionTest config = new QuarkusExtensionTest()
.withApplicationRoot((jar) -> jar
.addClasses(MyResource.class, MyService.class))
.overrideConfigKey("quarkus.some.key", "value");
- Use
.assertException(t -> assertThat(t).hasMessageContaining(...))for tests that expect build/startup failure - Use
.withEmptyApplication()for tests with no application classes - Use
.setExcludedDependencies()to remove extensions from classpath - Use
.setForcedDependencies()to add extensions to classpath - The test class itself is a CDI bean and can have
@Injectfields, which will be handled like any other bean.
Test Location
- Extension deployment tests:
extensions/<name>/deployment/src/test/ - Integration tests:
integration-tests/
Coding Style
- 4-space indentation (enforced by formatter)
- Never manually sort imports —
impsort-maven-pluginhandles it - Use JBoss Logging (
org.jboss.logging.Logger) - No
@authortags, no wildcard imports - Use
@ConfigMappinginterfaces for configuration - Use
String.format(Locale.ROOT, ...)—.formatted()is a forbidden API
Common Pitfalls
- Classloading: runtime code must never reference deployment classes