🔥 FREE PRO OFFER OnlyLink.click Pro Version is 100% Free of Cost till 31 December, 2026! Claim Free Pro

Dependency Injection in Quarkus with CDI: ArC, Scopes, Qualifiers & Build-Time Injection

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).

Traditional Runtime CDI with Reflection vs Quarkus ArC Build-Time Wiring

How ArC Differs from Standard CDI:

  1. Build-Time Augmentation: During mvn package or ./gradlew build, ArC scans all application beans, validates injection points, and pre-builds the dependency graph.
  2. Ahead-Of-Time (AOT) Bytecode Generation: ArC generates direct Java bytecode for instantiation, bean proxies, and interceptors upfront, eliminating reflection at runtime.
  3. 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.
  4. 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.

Quarkus CDI Bean Scopes and Lifecycles Diagram

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.

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 @Inject annotation 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.

Ghaznix Ecosystem Products

Empower Your Digital Presence & Workflows

Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.