Learn Quarkus - Dependency Injection & CDI
Episode 4 of 24

Learn Quarkus - Dependency Injection & CDI

This episode covers CDI in Quarkus thoroughly: the @Inject annotation and bean scopes, producers and qualifiers, alternative beans, bean lifecycle, and using interceptors to separate cross-cutting concerns.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

In episode 3 you ran your first Quarkus application. Now it's time to understand the engine that connects every component: CDI (Contexts and Dependency Injection). Quarkus uses a CDI implementation called ArC that is processed at build time.

Dependency injection is more than just a convenient pattern — it's the core architecture of a Quarkus application. Service beans, repositories, and resources are all wired together through CDI. This episode covers @Inject, bean scopes, producers, qualifiers, alternatives, lifecycle, and interceptors in depth.

CDI Basics: @Inject and Bean Scopes

Beans and the ArC Container

A bean is a class managed by the CDI container. The container creates instances, manages their lifecycle, and injects them where needed. In Quarkus, the ArC container discovers beans at build time, so there's no scanning overhead at runtime.

The Most Common Scopes

A scope determines how long a bean lives:

  • @ApplicationScoped: one instance per application. The most common for stateless services.
  • @Singleton: one instance, without a proxy.
  • @RequestScoped: one instance per HTTP request.
  • @Dependent: follows the scope of its owner.

An example bean with a scope:

JavaService bean with scope
import jakarta.enterprise.context.ApplicationScoped;
 
@ApplicationScoped
public class GreetingService {
 
    public String sapa(String nama) {
        return "Halo, " + nama + "!";
    }
}

Injecting with @Inject

JavaUsing @Inject
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
 
@Path("/api/greeting")
public class GreetingResource {
 
    @Inject
    GreetingService greetingService;
 
    @GET
    public String sapa() {
        return greetingService.sapa("Quarkus");
    }
}

The field @Inject GreetingService greetingService asks the container to inject a GreetingService instance. Run it with ./mvnw quarkus:dev and open the endpoint to see the result.

Producers and Qualifiers

Producer Methods

Sometimes you need to create a bean from an object that has no empty constructor or from a third-party type. The solution is a producer method:

JavaProducer for external objects
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.Clock;
 
@ApplicationScoped
public class ClockProducer {
 
    @Produces
    Clock clock() {
        return Clock.systemUTC();
    }
}

The Clock bean can now be @Injected anywhere, and the instance is created by this producer method.

Qualifiers for Disambiguation

If there's more than one bean of the same type, use a qualifier. A qualifier is a custom annotation:

JavaCustom qualifier
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
@Qualifier
@Retention(RUNTIME)
@Target({FIELD, PARAMETER, METHOD})
public @interface Premium {}

Then attach this qualifier to the bean and to the injection point. For example, @Premium GreetingService service will select the bean annotated with @Premium.

Alternative Beans and Lifecycle

Marking Alternative Beans

Alternative beans are useful for activating a different implementation — for example, a mock implementation for testing:

JavaAlternative bean
import jakarta.annotation.Priority;
import jakarta.enterprise.inject.Alternative;
 
@Alternative
@Priority(1000)
public class GreetingServicePremium extends GreetingService {
    @Override
    public String sapa(String nama) {
        return "Halo, " + nama + "! Anda member premium.";
    }
}

A class can mark itself as @Alternative and be activated via @Priority. When two beans claim the same type, the bean with the smaller @Priority wins. Alternative selection can also be configured via quarkus.arc.selected-alternatives in application.properties.

Lifecycle Callbacks

Beans have a lifecycle you can hook into: the @PostConstruct method runs after dependencies are injected, and @PreDestroy runs before the bean is destroyed — the right place to open and close resources.

Interceptors

Interceptors separate cross-cutting concerns like logging, timing, or transactions from business logic. Quarkus provides built-in interceptors such as @Transactional (episode 10). You can also create your own interceptor using an interceptor binding annotation:

JavaInterceptor binding
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
@InterceptorBinding
@Retention(RUNTIME)
@Target({METHOD, TYPE})
public @interface Logged {}

Combining @ApplicationScoped and interceptors lets you add behavior to methods without changing their core logic. For applications with many third-party libraries, control ArC's bean discovery via quarkus.arc.exclude-types and quarkus.arc.unremovable-types. Best practice: use @ApplicationScoped as the default for stateless services, @RequestScoped for per-request state, and avoid @Singleton unless you truly need a proxy-free bean.

Wrap-Up

Episode 4 gives you full control over dependency injection in Quarkus: @Inject and bean scopes, producer methods for external types, qualifiers for disambiguation, alternative beans for implementation selection, lifecycle callbacks, interceptors for cross-cutting concerns, as well as ArC bean discovery configuration.

Key takeaways:

  • ArC is Quarkus' CDI container that works at build time.
  • @ApplicationScoped is the best default for stateless services.
  • Producer methods create beans from types without an empty constructor.
  • Qualifiers distinguish multiple beans of the same type.
  • @Alternative with @Priority selects the active implementation.
  • @PostConstruct and @PreDestroy manage the bean lifecycle.
  • Interceptor bindings separate cross-cutting concerns from business logic.

In episode 5 we'll build a REST API with RESTEasy Reactive — from mapping @GET, @POST, @PUT, @DELETE, and @Path, body binding and validation, to error handling with custom exception mappers.