Dependency Injection in Quarkus with CDI: ArC, Scopes, Qualifiers & Build-Time Injection
Dependency Injection (DI) is the bedrock of modern enterprise Java application development. For years, Java developers relied on Spring IoC (Inversion of Control) or standard Jakarta EE CDI (Contexts and Dependency Injection) containers like Hibernate Weld or OpenWebBeans to decouple component lifecycles, manage service dependencies, and inject configuration properties.
However, as enterprise applications transitioned toward cloud-native microservices, containerization with Docker and Kubernetes, and serverless execution models (AWS Lambda, Knative), traditional dependency injection containers exposed a critical flaw: heavy runtime reflection and high memory overhead.
Enter Quarkus and its lightweight CDI engine, ArC. Built from the ground up for Kubernetes and GraalVM native compilation, Quarkus redefines dependency injection by shifting reflection, classpath scanning, and bean graph wiring from application runtime to build time.
In this deep-dive guide, we will explore Dependency Injection in Quarkus with CDI, covering the inner mechanics of the ArC container, CDI scopes, custom qualifiers, producers, interceptors, event-driven observers, programmatic lookups, and best practices for writing high-performance cloud-native Java applications.
1. Understanding ArC: Quarkus Build-Time CDI Engine
Standard Jakarta EE CDI containers operate entirely at application startup. When a legacy application boots, the CDI container scans every JAR in the classpath, parses metadata, inspects annotations using Java Reflection (java.lang.reflect), generates dynamic proxies in memory, and builds the dependency graph. This dynamic reflection consumes significant RAM (Metaspace) and increases cold-start boot time.
Quarkus solves this bottleneck with ArC (a build-time CDI implementation based on Jakarta CDI 4.0 specifications).
How ArC Differs from Standard CDI:
- Build-Time Augmentation: During
mvn packageor./gradlew build, ArC scans all application beans, validates injection points, and pre-builds the dependency graph. - Ahead-Of-Time (AOT) Bytecode Generation: ArC generates direct Java bytecode for instantiation, bean proxies, and interceptors upfront, eliminating reflection at runtime.
- Unused Bean Pruning (Dead Code Elimination): Any CDI bean that is never injected or referenced dynamically is identified during compilation and automatically stripped from the final binary, reducing memory footprint.
- GraalVM Native Image Optimization: Because bean wiring is resolved at build time, Quarkus applications compile seamlessly into native executables with zero reflection configuration hints.
2. CDI Bean Scopes & Lifecycle Management
In CDI, a Scope dictates the lifecycle and visibility of a bean instance. Quarkus ArC supports standard Jakarta CDI scopes with build-time optimizations.
Primary CDI Scopes in Quarkus:
| Scope Annotation | Lifecycle & Behavior | Proxy Creation | Best Use Case |
|---|---|---|---|
@ApplicationScoped |
Single instance per application. Lazy initialization upon first invocation. State shared across all threads. | Client Proxy Generated | Stateless services, repositories, configurations. |
@RequestScoped |
Instance bound to a single HTTP request or event invocation. Destroyed when request completes. | Client Proxy Generated | User sessions, request context data, security tokens. |
@Dependent |
Default scope if none specified. Instance tied directly to the lifetime of the injecting bean. | No Proxy (Direct Instance) | Helper utility classes, ephemeral state holders. |
@Singleton |
Single instance per application. Instantiated eagerly or lazily. No client proxy wrapper. | No Proxy (Direct Instance) | High-performance singletons where client proxy overhead must be avoided. |
@Unremovable |
Marker annotation instructing ArC not to remove the bean during build-time optimization. | Depends on Scope | Dynamic lookup targets or reflection-referenced beans. |
Code Example: Declaring Scoped Beans
package com.ghaznix.services;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import java.util.UUID;
@ApplicationScoped
public class OrderProcessingService {
public String processOrder(String orderId) {
return "Order " + orderId + " processed successfully.";
}
}
@RequestScoped
public class RequestContextHolder {
private final String requestId = UUID.randomUUID().toString();
public String getRequestId() {
return requestId;
}
}
3. Dependency Injection Patterns in Quarkus
Quarkus supports multiple injection styles using the standard jakarta.inject.Inject annotation.
A. Constructor Injection (Recommended Best Practice)
Constructor injection guarantees immutability (final fields), simplifies unit testing without requiring CDI containers, and makes dependencies explicit.
package com.ghaznix.resources;
import com.ghaznix.services.OrderProcessingService;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@Path("/orders")
public class OrderResource {
private final OrderProcessingService orderService;
// In Quarkus, if a class has a single constructor, @Inject is OPTIONAL!
@Inject
public OrderResource(OrderProcessingService orderService) {
this.orderService = orderService;
}
@POST
@Path("/{id}")
public String checkout(@PathParam("id") String id) {
return orderService.processOrder(id);
}
}
Pro Tip: In Quarkus ArC, if a bean has only one constructor, you can omit the
@Injectannotation entirely. ArC automatically recognizes it as the injection constructor.
B. Field Injection
Field injection is concise but makes fields mutable and complicates standalone unit testing.
@ApplicationScoped
public class NotificationService {
@Inject
EmailClient emailClient; // Field injection
}
4. Qualifiers & Disambiguating Multiple Implementations
When multiple beans implement the same interface, CDI throws an ambiguous dependency exception at build time. Quarkus uses Qualifiers to resolve target bean implementations safely.
Defining Custom Qualifiers
package com.ghaznix.payment;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface PaymentType {
Mode value();
enum Mode {
CREDIT_CARD,
PAYPAL,
CRYPTO
}
}
Implementing Qualified Beans
public interface PaymentProcessor {
void process(double amount);
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.CREDIT_CARD)
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing Credit Card payment: $" + amount);
}
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.PAYPAL)
public class PayPalProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}
Injecting Qualified Beans
@ApplicationScoped
public class CheckoutService {
private final PaymentProcessor creditCardProcessor;
private final PaymentProcessor payPalProcessor;
@Inject
public CheckoutService(
@PaymentType(PaymentType.Mode.CREDIT_CARD) PaymentProcessor creditCardProcessor,
@PaymentType(PaymentType.Mode.PAYPAL) PaymentProcessor payPalProcessor) {
this.creditCardProcessor = creditCardProcessor;
this.payPalProcessor = payPalProcessor;
}
}
5. Producers & Disposers: Injecting Third-Party Objects
Sometimes you need to inject classes from third-party libraries (e.g., AWS SDK clients, Redis connections, custom configuration beans) that are not annotated with @ApplicationScoped. Quarkus handles this using Producer Methods and Disposers.
package com.ghaznix.config;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@ApplicationScoped
public class DatabaseConnectionProducer {
@Produces
@ApplicationScoped
public Connection createDatabaseConnection() throws SQLException {
return DriverManager.getConnection("jdbc:postgresql://localhost:5432/mydb", "user", "pass");
}
// Disposer method called automatically when bean is destroyed
public void closeConnection(@Disposes Connection connection) throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
}
6. CDI Interceptors: Aspect-Oriented Programming (AOP)
Interceptors allow cross-cutting concerns (logging, audit trails, metrics, rate-limiting) to be attached to bean methods without polluting business logic.
Step 1: Define Interceptor Binding
package com.ghaznix.interceptors;
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@InterceptorBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface LogExecutionTime {
}
Step 2: Implement Interceptor Logic
package com.ghaznix.interceptors;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@LogExecutionTime
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LogExecutionTimeInterceptor {
@AroundInvoke
public Object logInvocation(InvocationContext context) throws Exception {
long startTime = System.currentTimeMillis();
try {
return context.proceed();
} finally {
long duration = System.currentTimeMillis() - startTime;
System.out.printf("[AUDIT] Method %s executed in %d ms%n",
context.getMethod().getName(), duration);
}
}
}
Step 3: Annotate Target Beans
@ApplicationScoped
public class AnalyticsService {
@LogExecutionTime
public void computeAggregates() {
// Business computation
}
}
Because Quarkus generates static interceptor proxies at build time, method invocation overhead is virtually zero compared to dynamic CGLIB runtime proxies in Spring Boot.
7. Event-Driven Architecture with CDI Events
CDI provides built-in synchronous and asynchronous event publishing for decoupled in-process communication.
Publishing & Observing Events
// 1. Define Event Payload
public record OrderCreatedEvent(String orderId, double totalAmount) {}
// 2. Publish Event
@ApplicationScoped
public class OrderService {
@Inject
Event<OrderCreatedEvent> orderEventPublisher;
public void createOrder(String orderId, double amount) {
// Create order logic...
orderEventPublisher.fire(new OrderCreatedEvent(orderId, amount));
}
}
// 3. Observe Event Synchronously or Asynchronously
@ApplicationScoped
public class InventoryObserver {
public void onOrderCreated(@Observes OrderCreatedEvent event) {
System.out.println("Updating inventory for order: " + event.orderId());
}
public void onOrderCreatedAsync(@ObservesAsync OrderCreatedEvent event) {
System.out.println("Async audit log for order: " + event.orderId());
}
}
Application Lifecycle Events
Quarkus exposes startup and shutdown events to execute initialization code when the application launches or terminates:
@ApplicationScoped
public class ApplicationLifecycleBean {
void onStart(@Observes StartupEvent ev) {
System.out.println("Quarkus Application starting up...");
}
void onStop(@Observes ShutdownEvent ev) {
System.out.println("Quarkus Application shutting down...");
}
}
8. Programmatic Bean Lookup with Instance<T> & Arc.container()
When target implementations must be chosen dynamically at runtime (or inside legacy non-CDI instances), use Instance<T> or Quarkus Arc.container() static lookup.
@ApplicationScoped
public class DynamicPluginRegistry {
@Inject
Instance<PaymentProcessor> processors;
public void executeAll(double amount) {
for (PaymentProcessor processor : processors) {
processor.process(amount);
}
}
}
9. Architectural Comparison: Quarkus ArC vs Spring IoC vs Weld
| Feature | Quarkus ArC | Spring IoC Container | Standard Jakarta Weld |
|---|---|---|---|
| Dependency Resolution | Build-Time (AOT) | Runtime Reflection | Runtime Reflection |
| Metaspace RAM Overhead | Extremely Low (~0MB for metadata) | High (Dynamic Proxy Class Cache) | Moderate to High |
| Startup Time | Milliseconds (< 0.05s) | Seconds (2.5s - 8s) | Seconds (1.5s - 4s) |
| GraalVM Native Support | Native Out-of-the-box | Requires Spring Native Hints | Complex configuration |
| Unused Bean Pruning | Automatic Build-Time Pruning | Kept in Classpath | Kept in Classpath |
| Single Constructor Injection | Implicit (No annotation needed) | Supported | Requires @Inject |
10. Unit Testing CDI Beans with @QuarkusTest and @InjectMock
Quarkus makes testing CDI beans seamless with JUnit 5 and RestAssured. Use @InjectMock to replace dependency beans with Mockito mocks automatically during testing.
package com.ghaznix;
import com.ghaznix.services.OrderProcessingService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusTest
public class OrderServiceTest {
@Inject
OrderService orderService;
@InjectMock
OrderProcessingService orderProcessingService;
@Test
public void testOrderCheckout() {
Mockito.when(orderProcessingService.processOrder("ORD-101"))
.thenReturn("Mocked Success");
String result = orderService.checkout("ORD-101");
Assertions.assertEquals("Mocked Success", result);
}
}
11. Conclusion
Dependency Injection in Quarkus with CDI (ArC) marks a monumental shift in enterprise Java design. By transferring bean metadata scanning, proxy generation, and injection graph resolution to compilation time, Quarkus delivers supersonic startup speeds, subatomic memory footprints, and seamless GraalVM native binary support.
Whether you are migrating from Spring Boot or building greenfield cloud-native microservices on Kubernetes, mastering Quarkus CDI equips you with the performance and developer productivity required for modern software architecture.
Tags
Empower Your Digital Presence & Workflows
Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.