What is CDI, and How Does it Relate to Spring DI?
CDI (Contexts and Dependency Injection) is the Jakarta EE
standard specification for dependency injection and contextual lifecycle
management. Like Spring IoC, it solves the same core problem: instead of
your code manually constructing dependencies with new, a
container creates, wires, and manages the lifecycle of your objects.
The key distinction: CDI is a specification (defined in
jakarta.inject.* and jakarta.enterprise.context.*),
implemented by containers like Weld (the reference implementation, used in
WildFly) and OpenWebBeans (used in TomEE). Spring DI is a
framework implementation — it predates CDI and inspired
parts of it, but follows its own model. When you use Quarkus, you're using
CDI directly (via ArC, Quarkus's CDI implementation). When you use Spring
Boot, you're using Spring DI — though Spring also honours
@Inject from jakarta.inject as an alternative to
@Autowired.
// BEFORE: tight coupling — impossible to test without a real Database
public class OrderService {
private PaymentService payment = new PaymentService(); // hard dependency
private EmailService email = new EmailService(); // another hard dependency
}
// AFTER CDI: loose coupling — container injects; test can inject mocks
@ApplicationScoped
public class OrderService {
private final PaymentService payment;
private final EmailService email;
@Inject // constructor injection — preferred, same as Spring
public OrderService(PaymentService payment, EmailService email) {
this.payment = payment;
this.email = email;
}
}
| Aspect | CDI (Jakarta EE) | Spring DI |
|---|---|---|
| Nature | Specification — multiple implementations | Framework — one implementation |
| Core inject annotation | @Inject (jakarta.inject) |
@Autowired (Spring-specific); also supports @Inject |
| Scope model | Contextual — beans are contextual instances tied to a lifecycle | Bean definition — prototype, singleton, request, session |
| Disambiguation | @Qualifier (custom annotation) |
@Qualifier (string-based) or @Primary |
| Proxy model | Client proxies always — even for @ApplicationScoped |
CGLIB proxies only when AOP is involved |
| Used by | Quarkus, WildFly, Payara, TomEE | Spring Boot, Spring Framework |
Injection Styles — and Why Constructor Is the Right Default
// ✅ Constructor injection — preferred in CDI and Spring alike
@ApplicationScoped
public class ReportService {
private final UserRepository users;
private final PdfGenerator pdf;
@Inject
public ReportService(UserRepository users, PdfGenerator pdf) {
this.users = users;
this.pdf = pdf;
// fields are final — can't accidentally be null or reassigned
}
}
// ⚠️ Field injection — tempting but problematic
@ApplicationScoped
public class ReportService {
@Inject private UserRepository users; // can't be final
@Inject private PdfGenerator pdf; // invisible dependencies in unit tests
// to unit test, you need a CDI container or reflection-based injection
}
With constructor injection you can test without any container at all:
new ReportService(mockUsers, mockPdf). With field
injection, the fields are private and injected by the container —
in a plain unit test they remain null. You either need a
CDI test runner (slow), or you resort to reflection-based injection
in tests (fragile). Constructor injection is not just style; it's a
testability constraint. This is identical to the argument Spring
makes for constructor injection over @Autowired on
fields.
// Setter injection — for optional dependencies only
@ApplicationScoped
public class NotificationService {
private SmsGateway smsGateway; // optional — may not be configured in all envs
@Inject
public void setSmsGateway(@Any Instance<SmsGateway> gateway) {
if (gateway.isResolvable()) {
this.smsGateway = gateway.get();
}
}
}
CDI Scopes — How Long a Bean Lives
@RequestScoped // one instance per HTTP request — destroyed when the request ends
@SessionScoped // one instance per HTTP session — must implement Serializable
@ApplicationScoped // one instance for the app lifetime — effectively a singleton
@ConversationScoped // developer-controlled: spans multiple requests when explicitly kept alive
@Dependent // default: one instance per injection point — no shared state
// @RequestScoped — data that belongs to exactly one request
@RequestScoped
public class RequestContext {
private String correlationId = UUID.randomUUID().toString();
private Instant requestStart = Instant.now();
// getters, no setters — this data doesn't change within one request
}
// @SessionScoped — user state across multiple requests
@SessionScoped
public class UserSession implements Serializable { // Serializable required for clustering
private Long userId;
private String username;
private Set<String> roles = new HashSet<>();
// keep small — sessions are replicated across cluster nodes
}
// @ApplicationScoped — shared state or heavyweight resources, initialised once
@ApplicationScoped
public class ConfigService {
private Map<String, String> config;
@PostConstruct
public void load() {
config = loadConfigFromDatabase(); // runs once — result cached for app lifetime
}
}
Injecting a shorter-lived bean into a
longer-lived one is a scope mismatch. The most common
case: injecting @RequestScoped directly into
@ApplicationScoped. The application-scoped bean is created
once; the request-scoped bean changes per request. If CDI injected the
real @RequestScoped instance, the application-scoped bean
would hold a stale reference after the first request ends. CDI solves
this with a client proxy — see the next section. The bug manifests when
a class is final or has no no-arg constructor, making it
impossible to proxy, and CDI throws a deployment exception at startup.
Client Proxies — How CDI Handles Scope Differences
CDI almost never injects the real bean instance into another bean. It injects a client proxy — a generated subclass that intercepts every method call and forwards it to the correct real instance for the current context. This is what makes scope mismatches safe and what enables interceptors to work transparently.
/*
* @ApplicationScoped bean holds a reference to a @RequestScoped bean:
*
* ┌─────────────────────────────────┐
* │ ConfigService (@ApplicationScoped) │
* │ │
* │ @Inject │
* │ RequestContext ctx; │ ← this is NOT a real RequestContext
* │ │ it's a CLIENT PROXY
* └─────────────────────────────────┘
* │
* │ every method call is forwarded by the proxy
* ▼
* ┌──────────────────────────────────────────────────────┐
* │ Proxy looks up the correct RequestContext instance │
* │ for the CURRENT thread / HTTP request │
* │ and delegates the call there │
* └──────────────────────────────────────────────────────┘
*
* Request 1 → proxy resolves to RequestContext instance A
* Request 2 → proxy resolves to RequestContext instance B
* The proxy reference in ConfigService never changes.
*/
@ApplicationScoped
public class AuditService {
@Inject
private RequestContext ctx; // a proxy — safe to hold in @ApplicationScoped
public void log(String action) {
// ctx.getCorrelationId() calls through the proxy → correct per-request instance
log.info("[{}] {}", ctx.getCorrelationId(), action);
}
}
Because CDI generates a subclass as the proxy, the proxied class must satisfy three requirements:
- The class must not be
final— afinalclass cannot be subclassed - No method must be
final— final methods cannot be overridden in the subclass - There must be a no-arg constructor (can be package-private or protected) — the proxy needs to instantiate without arguments
Violation is a deployment error at startup — a
javax.enterprise.inject.spi.DeploymentException — not a
NullPointerException at runtime. This is better than it
sounds: it fails fast and loudly, before any request is served. The
same three constraints apply to Spring's CGLIB proxies (used for
@Transactional, @Async, etc.) — if you've
ever hit "cannot subclass final class" from Spring AOP, this is the
exact same mechanism.
Qualifiers — Resolving Multiple Implementations
When two or more beans implement the same type, CDI can't choose
automatically — injecting without a qualifier is an ambiguous dependency
and fails at deployment. @Qualifier annotations are the
disambiguation mechanism.
// Define qualifier annotations
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER})
public @interface Smtp {}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER})
public @interface Mock {}
// Two implementations
@Smtp @ApplicationScoped
public class SmtpEmailService implements EmailService {
public void send(String to, String body) { /* real SMTP */ }
}
@Mock @ApplicationScoped
public class MockEmailService implements EmailService {
public void send(String to, String body) {
log.info("[MOCK] Email to {}: {}", to, body);
}
}
// Inject the one you need
@ApplicationScoped
public class NotificationService {
private final EmailService emailService;
@Inject
public NotificationService(@Smtp EmailService emailService) {
this.emailService = emailService;
}
}
CDI qualifiers are custom annotations — compile-time type-safe, refactoring-friendly. Spring's @Qualifier("smtp") takes a string — a typo silently fails at runtime. Spring also offers @Primary for "this is the default when nothing else is specified," which CDI achieves with the built-in @Default qualifier. Both models work; CDI's is strictly safer.
Producers — Creating Beans With Custom Logic
Not every object can be annotated with a CDI scope — third-party classes,
objects that require runtime parameters to construct, or objects that need
a factory method. @Produces turns a method or field into a
bean factory that CDI manages.
@ApplicationScoped
public class Infrastructure {
// Produce a configured ObjectMapper as a CDI bean
@Produces
@ApplicationScoped
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
// Produce a config value from a property file
@Produces
@Named("apiBaseUrl")
public String apiBaseUrl() {
return System.getenv().getOrDefault("API_BASE_URL", "http://localhost:8080");
}
}
// Now ObjectMapper is injectable anywhere as a CDI bean
@ApplicationScoped
public class JsonConverter {
private final ObjectMapper mapper;
@Inject
public JsonConverter(ObjectMapper mapper) { this.mapper = mapper; }
}
CDI Events — Decoupled Publish/Subscribe
CDI's event system allows beans to communicate without knowing about each
other. The publisher fires an event object; any bean with an
@Observes method for that type receives it. No wiring, no
interface contract — pure type-based dispatch.
// Event payload — a plain class, no CDI annotation needed
public record UserRegisteredEvent(Long userId, String email) {}
// Publisher — injects Event<T> and fires it
@ApplicationScoped
public class RegistrationService {
@Inject
private Event<UserRegisteredEvent> registrationEvent;
public void register(String email) {
User user = createUser(email);
registrationEvent.fire(new UserRegisteredEvent(user.getId(), email));
// all @Observes methods run synchronously before this line returns
}
}
// Observers — registered automatically, no explicit subscription needed
@ApplicationScoped
public class WelcomeEmailSender {
public void onRegistration(@Observes UserRegisteredEvent event) {
sendWelcomeEmail(event.email());
}
}
@ApplicationScoped
public class AnalyticsTracker {
public void onRegistration(@Observes UserRegisteredEvent event) {
track("user_registered", event.userId());
}
}
// Async event — fire and continue, observer runs in a separate thread
public void registerAsync(String email) {
User user = createUser(email);
registrationEvent.fireAsync(new UserRegisteredEvent(user.getId(), email));
// returns immediately — observers run concurrently
}
Spring's equivalent is ApplicationEventPublisher +
@EventListener. The model is identical — publisher fires,
observers react, no direct coupling. CDI's version is arguably cleaner:
Event<T> is injected as a typed dependency, and the
observer method signature is the only "contract." Spring's
@EventListener requires the event class to extend
ApplicationEvent or be a plain object (since Spring 4.2).
Both work; CDI's doesn't require any base class at all.
Interceptors — Cross-Cutting Concerns Without Inheritance
// Step 1: define an interceptor binding annotation
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Timed {}
// Step 2: implement the interceptor
@Timed
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class TimingInterceptor {
@AroundInvoke
public Object measure(InvocationContext ctx) throws Exception {
long start = System.nanoTime();
try {
return ctx.proceed(); // invoke the actual method
} finally {
long ms = (System.nanoTime() - start) / 1_000_000;
log.debug("{}.{}() took {}ms",
ctx.getTarget().getClass().getSimpleName(),
ctx.getMethod().getName(), ms);
}
}
}
// Step 3: apply it with just one annotation
@ApplicationScoped
public class ReportService {
@Timed // interceptor runs transparently — zero change to business logic
public Report generate(Long userId) { ... }
}
Spring @Transactional, @Async, and
@Cacheable are all implemented via Spring AOP, which uses
CGLIB proxies — mechanically identical to CDI's client proxy model.
CDI interceptors are the Jakarta standard for the same pattern. The
practical difference: Spring AOP integrates with Spring's own
annotations; CDI interceptors are portable across any CDI container
(Quarkus, WildFly, TomEE) without Spring.
Bean Discovery — What CDI Scans and What It Ignores
CDI needs to know which classes to consider as beans. This is controlled by
bean-discovery-mode in beans.xml, or by the
absence of that file entirely.
<!-- src/main/resources/META-INF/beans.xml (or WEB-INF/beans.xml in a WAR) -->
<!-- annotated (recommended since CDI 1.1 / EE 7) -->
<!-- Only classes with a bean-defining annotation are considered beans -->
<beans xmlns="https://jakarta.ee/xml/ns/jakartaee"
version="4.0"
bean-discovery-mode="annotated">
</beans>
<!-- all — every class in the archive is a potential bean, even without annotations -->
<!-- Only use this for legacy compatibility; slow startup, unexpected beans -->
<beans bean-discovery-mode="all"></beans>
<!-- none — CDI disabled for this archive entirely -->
<beans bean-discovery-mode="none"></beans>
Bean-defining annotations (the ones that make a class
discoverable in annotated mode):
| Annotation | Source |
|---|---|
@ApplicationScoped, @RequestScoped, @SessionScoped, @ConversationScoped | jakarta.enterprise.context |
@Dependent | jakarta.enterprise.context |
@Singleton | jakarta.inject |
@Interceptor, @Decorator | jakarta.interceptor |
Any custom @NormalScope or @Stereotype | User-defined |
In CDI 4.0 (Jakarta EE 11), the absence of beans.xml
means the archive is treated as an implicit bean archive with
annotated discovery mode — same as having an empty
beans.xml with bean-discovery-mode="annotated".
An explicit beans.xml is only required when you need
all mode or none, or when you need to include
portable extensions and interceptors via XML.
Interview Questions
Q: What problem does CDI solve, and what is the difference between @Inject and new?
CDI solves tight coupling — when you use new to create a
dependency, your class controls its creation and you can't swap it without
modifying the class. With @Inject, the CDI container provides
the instance, allowing you to change the implementation, apply interceptors,
and inject mocks in tests — none of which require changing the class that
depends on it.
Q: What is the difference between @ApplicationScoped and @RequestScoped?
@ApplicationScoped creates one instance for the entire
application lifetime — effectively a singleton. @RequestScoped
creates one instance per HTTP request and destroys it when the request ends.
Data that belongs to one user request goes in @RequestScoped
beans; shared configuration or caches go in @ApplicationScoped
beans.
Q: What is a CDI client proxy and what three constraints does it impose on proxied classes?
When a longer-lived bean injects a shorter-lived one, CDI injects a
generated subclass (client proxy) that forwards every method call to the
correct real instance for the current context — e.g. the right
@RequestScoped instance for the current HTTP thread. Because
the proxy is a subclass, the target class must not be final,
none of its methods must be final, and it must have a no-arg
constructor. Violating any of these produces a deployment exception at
startup — the same constraints Spring's CGLIB proxies impose for
@Transactional and @Async.
Q: How do CDI qualifiers differ from Spring's @Qualifier, and why does the difference matter?
CDI qualifiers are custom annotations — compile-time type-safe, IDE-
refactorable, and can carry members (attributes) for fine-grained
disambiguation. Spring's @Qualifier("smtpEmail") takes a
string — a typo produces an UnsatisfiedDependencyException at
startup (or worse, silently picks the wrong bean if names overlap). CDI's
model eliminates the string entirely: if you rename the qualifier
annotation, the compiler catches every usage site.
Q: What is bean-discovery-mode="annotated" and why is it preferred over "all"?
In annotated mode, only classes carrying a bean-defining
annotation (@ApplicationScoped, @RequestScoped,
etc.) are eligible as CDI beans. In all mode, every class in
the archive is a potential bean — including third-party library classes,
utility classes, and value objects — which causes slower startup, unexpected
bean resolution conflicts, and proxying attempts on classes that can't be
proxied. annotated is the explicit, predictable default and
is the CDI 4.0 implicit mode when no beans.xml exists.