CDI and the Programming Model (ArC)
|
This section documents Quarkus 3.39.x (current) with 3.33.x LTS as the production recommendation, on the Java 17+ baseline, as published at the official Quarkus guides — which are the reference these pages are written and verified against. Quarkus 4 raises the baseline to Java 21 (first release candidate planned for end of September 2026, see the Quarkus blog); readers upgrading past 3.39.x/3.33.x LTS should expect that change. This content was generated with the assistance of AI and should be verified against those official guides before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
ArC is Quarkus’s own implementation of Jakarta CDI (Contexts and Dependency Injection) — not an embedded copy of Weld, the reference runtime CDI container most Jakarta EE developers already know. ArC resolves as much of the bean graph as possible at build time: which beans exist, which scope each one lives in, and which concrete beans satisfy each injection point are all decided before the application ever starts, and the generated bytecode reflects that decision directly instead of discovering it through reflection at runtime. This is the single biggest reason a Quarkus application starts as fast as it does, and it shapes almost every rule in this page: bean discovery is annotation-driven rather than exhaustive classpath scanning, unused beans are pruned rather than kept "just in case", and the container is a closed world once the application is built. This page covers how ArC discovers and scopes beans, the injection and extension points CDI offers (producers, qualifiers, alternatives, stereotypes, interceptors, decorators), the CDI eventing model, and where ArC deliberately stops short of full runtime CDI.
Bean discovery: annotations in, unused beans out
A traditional CDI container can be configured to scan an entire archive for beans via an empty beans.xml
(bean-discovery-mode="all"). ArC never does this: a class is only considered a bean candidate if it carries a
bean-defining annotation — a scope annotation (@ApplicationScoped, @RequestScoped, @Singleton,
@Dependent, @SessionScoped), a stereotype, or an interceptor/decorator annotation. A plain class with no such
annotation is simply not a bean, no matter how it is packaged:
package com.example.orders;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class OrderPricingService {
@jakarta.inject.Inject
TaxCalculator taxCalculator;
public java.math.BigDecimal priceWithTax(java.math.BigDecimal net) {
return net.add(taxCalculator.taxFor(net));
}
}
Discovery being annotation-driven at build time also means ArC can safely do something a runtime container
cannot: remove beans nobody actually uses. By default, any discovered bean that is never injected, never looked
up programmatically, and not otherwise reachable is treated as removable and stripped from the generated
application to shrink its footprint and startup work. A bean is kept regardless — unremovable — when it is
looked up dynamically (CDI.current().select(…)), extended by another Quarkus extension that reads it via
reflection, or explicitly marked:
package com.example.orders;
import io.quarkus.arc.Unremovable;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
@Unremovable
public class MetricsRegistryBean {
// kept even though nothing in this codebase injects it directly --
// e.g. it is only found reflectively by another extension
}
|
Because bean removal and injection-point resolution both happen at build time, ArC needs to see the whole application graph before it packages anything — it cannot register a brand-new bean type after the application has started the way a runtime container can. This closed-world constraint is the same one behind Quarkus’s build-time augmentation model, covered in depth on the dedicated build-time augmentation page in this section. |
Scopes
Every CDI bean lives in exactly one scope, which governs how many instances exist and how long each one lives:
| Scope | Behavior |
|---|---|
|
One shared instance for the entire application lifetime. This is the default, most-used scope in Quarkus
applications. ArC backs it with a client proxy, so an injected reference is actually a lightweight proxy that
resolves to the real contextual instance on each call — this is what lets an |
|
One instance per request context (an HTTP request, or another unit of work Quarkus binds a request context to,
such as a message being processed). Also proxied, for the same reason as |
|
Also one instance for the application lifetime, but without a proxy: injection points hold a direct reference
to the real instance. That makes it marginally faster to call through and avoids proxy-related restrictions
(e.g. on |
|
The scope a bean gets when no scope annotation is present at all — |
|
One instance per HTTP session. Only meaningful for stateful, browser-session-backed web scenarios, and used sparingly in Quarkus applications, whose architecture generally favors stateless request handling over server-side session state. |
package com.example.orders;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Singleton;
@RequestScoped
public class RequestAuditContext {
// one instance per request; safe to hold request-specific mutable state
}
@Singleton
public class ClockProvider {
// one instance for the application's lifetime, injected without a proxy
java.time.Clock clock() {
return java.time.Clock.systemUTC();
}
}
@Dependent
public class ReportBuilder {
// a fresh instance every time this type is injected
}
Injection styles
Field injection is fully idiomatic in Quarkus/ArC, and is the style most Quarkus example code and generated
project templates use by default — unlike DI cultures built around a runtime proxying container, where field
injection is often discouraged because it hides dependencies inside an object that can be constructed
"half-wired" by new. That specific risk does not transfer to ArC in the same way: ArC generates the bean
graph and the proxy/direct-call bytecode at build time, so a field-injected bean is exactly as inspectable,
testable (via @QuarkusTest and @InjectMock), and safe to reason about as a constructor-injected one. Both
styles are fully supported:
package com.example.orders;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
// field injection: idiomatic and common in Quarkus
@ApplicationScoped
public class InvoiceService {
@Inject
OrderRepository orderRepository;
@Inject
OrderPricingService pricingService;
}
// constructor injection: also fully supported, and still often preferred
// when immutability (final fields) matters to the design
@ApplicationScoped
public class ShippingService {
private final OrderRepository orderRepository;
@Inject
public ShippingService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
Producers and disposers
A producer creates a bean from something the application doesn’t own the source of — a third-party class, or an object that needs to be built rather than simply constructed:
package com.example.orders;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;
@ApplicationScoped
public class ObjectMapperProducer {
@Produces
@ApplicationScoped
public com.fasterxml.jackson.databind.ObjectMapper objectMapper() {
return new com.fasterxml.jackson.databind.ObjectMapper()
.findAndRegisterModules();
}
public void closeMapper(@Disposes com.fasterxml.jackson.databind.ObjectMapper mapper) {
// paired cleanup hook, invoked when the produced bean's context ends
}
}
@Disposes pairs a cleanup method with a @Produces method of the same type — useful for producers whose
result holds a resource (a connection, a file handle) that needs an explicit close rather than relying on
garbage collection.
Qualifiers
A qualifier disambiguates between multiple beans of the same type. It is itself a meta-annotation, built with
CDI’s own @Qualifier:
package com.example.orders;
import jakarta.inject.Qualifier;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD, ElementType.TYPE})
public @interface Express {
}
package com.example.orders;
import jakarta.enterprise.context.ApplicationScoped;
@Express
@ApplicationScoped
public class ExpressShippingCalculator implements ShippingCalculator {
// one of possibly several ShippingCalculator implementations
}
package com.example.orders;
import jakarta.inject.Inject;
public class CheckoutService {
@Inject
@Express
ShippingCalculator shippingCalculator;
}
Alternatives, priorities, and stereotypes
@Alternative combined with @Priority overrides which concrete bean satisfies an injection point without
touching the original implementation — typically used to swap in a test double or an environment-specific
implementation:
package com.example.orders;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
@Alternative
@Priority(1)
@ApplicationScoped
public class InMemoryShippingCalculator implements ShippingCalculator {
// takes precedence over the default implementation while active
}
A stereotype bundles several annotations (a scope, qualifiers, interceptor bindings) into one, so beans that should always share the same combination don’t have to repeat it:
package com.example.orders;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Stereotype;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
@Stereotype
@ApplicationScoped
@Retention(RetentionPolicy.RUNTIME)
@Target(TYPE)
public @interface DomainService {
// any @DomainService-annotated class is @ApplicationScoped, with room
// to add shared interceptor bindings later without touching each bean
}
Interceptors and decorators
An interceptor wraps method calls matched by an interceptor-binding annotation, functionally similar to a
Spring @Around aspect but defined through CDI’s own SPI:
package com.example.orders;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@LogExecutionTime
@Interceptor
public class LogExecutionTimeInterceptor {
@AroundInvoke
Object logTime(InvocationContext context) throws Exception {
long start = System.nanoTime();
try {
return context.proceed();
} finally {
long elapsedMs = (System.nanoTime() - start) / 1_000_000;
System.out.println(context.getMethod() + " took " + elapsedMs + " ms");
}
}
}
A decorator (@Decorator) instead wraps a bean at the level of a business interface it implements, adding
behavior before/after delegating to the wrapped instance — useful when the added behavior is itself naturally
expressed as another implementation of the same interface, rather than as generic around-advice.
CDI events
CDI’s observer pattern lets one bean fire an event without knowing who, if anyone, is listening:
package com.example.orders;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event;
import jakarta.inject.Inject;
@ApplicationScoped
public class OrderService {
@Inject
Event<OrderPlaced> orderPlacedEvent;
void placeOrder(Order order) {
// ... persist the order ...
orderPlacedEvent.fire(new OrderPlaced(order.id()));
}
}
package com.example.orders;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
@ApplicationScoped
public class OrderPlacedNotifier {
void onOrderPlaced(@Observes OrderPlaced event) {
// runs synchronously on the firing thread by default;
// ObservesAsync/fireAsync exist for the asynchronous variant
}
}
Startup and shutdown
Quarkus fires its own lifecycle events through the same mechanism. StartupEvent and ShutdownEvent are raised
by the Quarkus runtime itself — once at application startup, once at graceful shutdown — and any bean can
observe them:
package com.example.orders;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
@ApplicationScoped
public class CacheWarmer {
void onStart(@Observes StartupEvent ev) {
// eagerly warm a cache at application startup
}
void onStop(@Observes ShutdownEvent ev) {
// release resources before the application exits
}
}
The @Startup annotation offers a shorter path to the same eager-initialization outcome when all that’s needed
is "construct this bean right away, don’t wait for its first use":
package com.example.orders;
import io.quarkus.runtime.Startup;
import jakarta.enterprise.context.ApplicationScoped;
@Startup
@ApplicationScoped
public class WarmStartCache {
// instantiated eagerly at startup instead of on first injection/lookup
}
|
|
Where ArC stops short of a runtime CDI container
ArC deliberately implements a subset of full Jakarta CDI, trading away features that are fundamentally incompatible with build-time resolution:
-
No dynamic bean registration after startup. A runtime container like Weld can, in principle, register new beans while the application is running (portable extensions doing so during container boot is normal even there); ArC’s bean graph is closed once the application is built, and no bean can be added afterward. This is the same closed-world model behind Quarkus’s build-time augmentation, referenced above.
-
Limited portable-extension SPI support. Full CDI’s
javax.enterprise.inject.spi/jakarta.enterprise.inject.spiportable extension mechanism assumes a runtime container performing its own classpath scanning; ArC supports only the subset of that SPI that is meaningful when bean discovery has already happened at build time, and Quarkus extensions typically integrate at the build-timeBuildSteplevel instead of through a portable CDI extension. -
Interceptors and decorators are resolved at build time, alongside everything else — the interceptor chain for a given method is fixed before the application starts rather than assembled dynamically per call.
Injected configuration participates in the same bean graph
Configuration values are themselves exposed as CDI beans: @ConfigProperty on a field or parameter, and
@ConfigMapping-annotated interfaces for structured configuration, are both resolved and injected through the
same ArC machinery as any other bean — a configuration value or mapping interface is just another injection
point in the graph ArC builds. The dedicated configuration page in this section covers @ConfigProperty and
@ConfigMapping in depth; the point worth keeping here is that configuration injection is not a separate,
parallel mechanism bolted onto CDI, it is CDI.
Build-time vs. runtime bean resolution
ArC finishes the left-hand path once, before the application ever runs; a traditional runtime container performs the right-hand path’s discovery-and-wiring work every time the application starts, and portable extensions can still influence it while it happens. That difference in when the graph is decided is the direct cause of Quarkus’s fast boot time discussed at the top of this page.