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.

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.
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.
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:
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class GreetingService {
public String sapa(String nama) {
return "Halo, " + nama + "!";
}
}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.
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:
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.
If there's more than one bean of the same type, use a qualifier. A qualifier is a custom annotation:
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 are useful for activating a different implementation — for example, a mock implementation for testing:
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.
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 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:
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.
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:
@ApplicationScoped is the best default for stateless services.@Alternative with @Priority selects the active implementation.@PostConstruct and @PreDestroy manage the bean lifecycle.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.