Jakarta CDI (Contexts & Dependency Injection)
CDI scopes, qualifiers, producers, events, and the lifecycle to actually understand.
# CLAUDE.md — Jakarta CDI
## Scopes
- `@ApplicationScoped` — one instance for the whole app. Use for stateless services.
- `@RequestScoped` — one per HTTP request. Use for request-tied state.
- `@SessionScoped` — one per HTTP session. Avoid in stateless APIs.
- `@Dependent` — same lifecycle as the consumer. Default for unannotated beans.
- `@Singleton` is **not the same** as `@ApplicationScoped` — `@Singleton` skips proxying. Stick with `@ApplicationScoped` unless you measure.
## Injection
- Constructor injection over field injection:
```java
@ApplicationScoped
public class CheckoutService {
private final OrderRepository orders;
private final PaymentGateway payments;
@Inject CheckoutService(OrderRepository orders, PaymentGateway payments) {
this.orders = orders;
this.payments = payments;
}
}
```
- Field injection (`@Inject` on a field) is allowed but harder to test and hides dependencies.
- Don't inject in `@PostConstruct` — too late; bean state is already half-initialized.
## Qualifiers
- When you have multiple beans of the same type, use a **qualifier annotation**:
```java
@Qualifier @Retention(RUNTIME) @Target({FIELD, PARAMETER})
public @interface Stripe {}
```
- Inject with `@Inject @Stripe PaymentGateway gateway`.
- Don't use `@Named("stripe")` for non-EL contexts. Qualifiers are type-checked; names are strings.
## Producers
- For third-party types you can't annotate, write a `@Produces` method:
```java
@Produces @ApplicationScoped
HttpClient httpClient() { return HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build(); }
```
- Producer methods can take parameters that themselves get injected.
- For cleanup, write a `@Disposes` method.
## Events
- CDI events decouple producers from consumers:
```java
@Inject Event<UserCreated> userCreated;
userCreated.fire(new UserCreated(user.id()));
```
- Observers: `void on(@Observes UserCreated event) { ... }`.
- For async: `userCreated.fireAsync(new UserCreated(...))`. Observer signature: `void on(@ObservesAsync UserCreated event)`.
- Don't make synchronous observers do slow work. They block the firing thread.
## Interceptors
- For cross-cutting concerns (logging, metrics, transactions). Built into the platform.
- Define an `@InterceptorBinding` annotation, an `@Interceptor` class, and apply the binding to methods.
- For most observability, **MicroProfile** + Jakarta EE interceptors give you what you need.
## Lifecycle hooks
- `@PostConstruct` — called once after injection completes. Use for initialization.
- `@PreDestroy` — called before the bean is destroyed. Use for cleanup.
- These hooks must not throw.
## Don't
- Don't put state in `@ApplicationScoped` beans without synchronization.
- Don't use `@Inject` to inject `@RequestScoped` beans into `@ApplicationScoped` ones unless you understand the proxy semantics.
- Don't use `BeanManager.getBeans` to look up beans dynamically. That's the service-locator anti-pattern.
- Don't put mutable static fields on CDI beans.
Other Jakarta EE templates
Modern Quarkus Rules
Quarkus 3+ defaults: dev mode, build-time optimization, configuration, and CDI.
Quarkus REST (RESTEasy Reactive)
JAX-RS endpoints with RESTEasy Reactive — request validation and response models.
Quarkus + Hibernate ORM with Panache
Active-record and repository styles with Panache, Flyway migrations, transactions.
Modern Jakarta EE Rules
Jakarta EE 10+ namespace migration, modular architecture, and platform conventions.
Jakarta Persistence (JPA)
Entities, fetch strategies, JPQL, transactions, and L2 cache discipline.