CDI (Contexts & Dependency Injection)

The Backbone of Jakarta EE Applications

← Back to Index

What is CDI?

Think of CDI like a restaurant with an automated service system:

CDI (Contexts and Dependency Injection) is a framework that automatically creates objects and injects dependencies for you. It's the core of Jakarta EE that ties everything together.

Key Benefits: No manual object creation (no 'new'), loose coupling, easier testing, lifecycle management

The Problem CDI Solves

// Without CDI - Manual creation (tight coupling)
public class OrderService {
    private PaymentService paymentService = new PaymentService();
    private EmailService emailService = new EmailService();

    // Hard to test, hard to change implementation
}

// With CDI - Automatic injection (loose coupling)
public class OrderService {
    @Inject
    private PaymentService paymentService;

    @Inject
    private EmailService emailService;

    // CDI provides instances automatically!
    // Easy to test, easy to swap implementations
}

Basic Dependency Injection with @Inject

Simple Example

// 1. Create a bean (just a regular class)
public class GreetingService {
    public String greet(String name) {
        return "Hello, " + name + "!";
    }
}

// 2. Inject it wherever needed
public class WelcomeController {
    @Inject  // CDI injects GreetingService automatically
    private GreetingService greetingService;

    public void welcome() {
        String message = greetingService.greet("Alice");
        System.out.println(message);  // Hello, Alice!
    }
}

What happened?

  1. You marked a field with @Inject
  2. CDI container creates a GreetingService instance
  3. CDI automatically assigns it to your field
  4. You use it without calling 'new'!

Three Ways to Inject

// 1. Field Injection (simplest, most common)
public class ServiceA {
    @Inject
    private Database database;
}

// 2. Constructor Injection (best for testing)
public class ServiceB {
    private final Database database;

    @Inject
    public ServiceB(Database database) {
        this.database = database;
    }
}

// 3. Setter Injection (optional dependencies)
public class ServiceC {
    private Database database;

    @Inject
    public void setDatabase(Database database) {
        this.database = database;
    }
}

CDI Scopes - Bean Lifecycles

Scopes control how long a bean lives and how many instances exist

1. @RequestScoped - One Instance Per HTTP Request

import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;

@Named  // Makes it accessible from JSP/JSF
@RequestScoped  // New instance for each HTTP request
public class ShoppingCartBean {
    private List<Product> items = new ArrayList<>();

    public void addItem(Product product) {
        items.add(product);
    }

    // Destroyed after request completes
}

2. @SessionScoped - One Instance Per User Session

import jakarta.enterprise.context.SessionScoped;
import java.io.Serializable;

@Named
@SessionScoped  // Lives across multiple requests from same user
public class UserSessionBean implements Serializable {
    private String username;
    private boolean loggedIn;

    public void login(String username) {
        this.username = username;
        this.loggedIn = true;
    }

    // Destroyed when session expires or user logs out
}

3. @ApplicationScoped - One Instance for Entire App

import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped  // Singleton - shared by all users
public class ConfigService {
    private Map<String, String> config = new HashMap<>();

    @PostConstruct
    public void init() {
        // Load config once when app starts
        config.put("app.name", "My App");
    }

    // Lives until application stops
}

4. @Dependent - New Instance Every Time (Default)

import jakarta.enterprise.context.Dependent;

@Dependent  // Default scope if no annotation
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }

    // New instance created every time it's injected
    // Destroyed with the bean that owns it
}
Scope Lifetime Instances Use Case
@Dependent Same as parent New every injection Stateless utilities
@RequestScoped One HTTP request One per request Form data, request processing
@SessionScoped User session One per user User login, shopping cart
@ApplicationScoped Entire app One (singleton) Config, caches, shared data
@ConversationScoped Multi-step flow One per conversation Wizards, multi-page forms

Qualifiers - Choosing Between Implementations

Problem: What if you have multiple implementations of an interface?

Define Qualifiers

import jakarta.inject.Qualifier;
import java.lang.annotation.*;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface CreditCard { }

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
public @interface PayPal { }

Use Qualifiers on Implementations

// Interface
public interface PaymentProcessor {
    void processPayment(double amount);
}

// Implementation 1
@CreditCard  // Qualifier
public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing $" + amount + " via Credit Card");
    }
}

// Implementation 2
@PayPal  // Qualifier
public class PayPalProcessor implements PaymentProcessor {
    @Override
    public void processPayment(double amount) {
        System.out.println("Processing $" + amount + " via PayPal");
    }
}

Inject Specific Implementation

public class CheckoutService {
    @Inject
    @CreditCard  // Inject CreditCardProcessor
    private PaymentProcessor creditCardProcessor;

    @Inject
    @PayPal  // Inject PayPalProcessor
    private PaymentProcessor payPalProcessor;

    public void checkout(String paymentMethod, double amount) {
        if ("credit".equals(paymentMethod)) {
            creditCardProcessor.processPayment(amount);
        } else {
            payPalProcessor.processPayment(amount);
        }
    }
}

Producers - Custom Bean Creation

Sometimes you need more control over how beans are created

@Produces Methods

public class DatabaseProducer {

    @Produces
    @ApplicationScoped
    public DataSource createDataSource() {
        // Custom creation logic
        DataSource ds = new DataSource();
        ds.setUrl("jdbc:postgresql://localhost/mydb");
        ds.setUsername("admin");
        ds.setPassword("secret");
        return ds;
    }

    @Produces
    @Named("maxConnections")
    public int getMaxConnections() {
        return 100;
    }
}

// Now you can inject them
public class UserRepository {
    @Inject
    private DataSource dataSource;  // Injected from producer

    @Inject
    @Named("maxConnections")
    private int maxConnections;
}

@Produces with Qualifiers

public class LoggerProducer {

    @Produces
    @Dependent
    public Logger createLogger(InjectionPoint injectionPoint) {
        // Automatically get logger for each class
        Class<?> targetClass = injectionPoint.getMember().getDeclaringClass();
        return Logger.getLogger(targetClass.getName());
    }
}

// Usage - each class gets its own logger
public class UserService {
    @Inject
    private Logger logger;  // Logger for UserService

    public void doSomething() {
        logger.info("UserService.doSomething called");
    }
}

@Disposes - Cleanup Resources

public class ResourceManager {

    @Produces
    @RequestScoped
    public Connection createConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:...");
    }

    public void closeConnection(@Disposes Connection connection) {
        try {
            if (connection != null && !connection.isClosed()) {
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

Lifecycle Callbacks

@PostConstruct and @PreDestroy

import jakarta.annotation.*;

@ApplicationScoped
public class CacheService {
    private Map<String, Object> cache;

    @PostConstruct  // Called AFTER bean creation and injection
    public void init() {
        System.out.println("Initializing cache...");
        cache = new HashMap<>();
        // Load initial data
        cache.put("key1", "value1");
    }

    @PreDestroy  // Called BEFORE bean destruction
    public void cleanup() {
        System.out.println("Cleaning up cache...");
        if (cache != null) {
            cache.clear();
        }
        // Save data, close connections, etc.
    }

    public Object get(String key) {
        return cache.get(key);
    }
}

Order of execution:

1. Constructor called
2. Dependencies injected (@Inject)
3. @PostConstruct method called ← Initialize here
4. Bean ready to use
   ... bean used by application ...
5. @PreDestroy method called ← Cleanup here
6. Bean destroyed

Events - Publish/Subscribe Pattern

Fire and Observe Events

// 1. Event class (just a POJO)
public class UserRegisteredEvent {
    private final String username;
    private final String email;

    public UserRegisteredEvent(String username, String email) {
        this.username = username;
        this.email = email;
    }

    public String getUsername() { return username; }
    public String getEmail() { return email; }
}

// 2. Fire event (publisher)
public class RegistrationService {
    @Inject
    private Event<UserRegisteredEvent> userRegisteredEvent;

    public void register(String username, String email) {
        // Register user in database...

        // Fire event
        userRegisteredEvent.fire(new UserRegisteredEvent(username, email));
    }
}

// 3. Observe event (subscribers)
public class EmailNotificationService {
    public void onUserRegistered(@Observes UserRegisteredEvent event) {
        System.out.println("Sending welcome email to " + event.getEmail());
        // Send email...
    }
}

public class AnalyticsService {
    public void onUserRegistered(@Observes UserRegisteredEvent event) {
        System.out.println("Tracking registration for " + event.getUsername());
        // Track analytics...
    }
}

What happens:

  1. User registers → RegistrationService.register() called
  2. Event fired → userRegisteredEvent.fire(event)
  3. All @Observes methods automatically called
  4. EmailNotificationService sends email
  5. AnalyticsService tracks data
  6. Completely decoupled!

Interceptors - Cross-Cutting Concerns

Add behavior to methods without changing the code

Create Interceptor Binding

import jakarta.interceptor.InterceptorBinding;

@InterceptorBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logged { }

Create Interceptor

import jakarta.interceptor.*;

@Logged
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LoggingInterceptor {

    @AroundInvoke
    public Object logMethod(InvocationContext context) throws Exception {
        String methodName = context.getMethod().getName();

        System.out.println("[LOG] Entering: " + methodName);
        long start = System.currentTimeMillis();

        try {
            // Proceed with the actual method call
            return context.proceed();
        } finally {
            long duration = System.currentTimeMillis() - start;
            System.out.println("[LOG] Exiting: " + methodName + " (" + duration + "ms)");
        }
    }
}

Use Interceptor

public class UserService {

    @Logged  // This method will be logged
    public User findUser(Long id) {
        // Your business logic
        return database.find(id);
    }

    @Logged
    public void saveUser(User user) {
        database.save(user);
    }
}

Output:

[LOG] Entering: findUser
[LOG] Exiting: findUser (23ms)
[LOG] Entering: saveUser
[LOG] Exiting: saveUser (45ms)

Common use cases: Logging, transactions, security, caching, performance monitoring

Best Practices

✅ DO:

  • Use constructor injection for required dependencies - Makes testing easier
  • Choose the right scope - @RequestScoped for request data, @ApplicationScoped for singletons
  • Use qualifiers for multiple implementations - Clear and explicit
  • Implement Serializable for @SessionScoped - Required for session replication
  • Use @PostConstruct for initialization - Not the constructor
  • Use events for loose coupling - Modules don't know about each other
  • Keep beans stateless when possible - Thread-safe and scalable

❌ DON'T:

  • Don't inject @RequestScoped into @ApplicationScoped - Scope mismatch!
  • Don't create beans with 'new' - CDI won't manage them
  • Don't forget @Named for JSF access - Required for view binding
  • Don't store heavy objects in @SessionScoped - Memory issues
  • Don't use field injection for testing - Use constructor injection
  • Don't put business logic in producers - Keep them simple

Summary

  • CDI is the dependency injection framework at the heart of Jakarta EE
  • @Inject automatically provides dependencies (no 'new' needed)
  • Scopes control bean lifecycle: @Dependent, @RequestScoped, @SessionScoped, @ApplicationScoped
  • @Qualifier distinguishes between multiple implementations
  • @Produces creates beans with custom logic
  • @PostConstruct/@PreDestroy lifecycle callbacks for init/cleanup
  • Events enable publish-subscribe pattern with @Observes
  • Interceptors add cross-cutting concerns (logging, security, etc.)
  • Benefits: Loose coupling, easier testing, automatic lifecycle management
  • Similar to: Spring's @Autowired but standards-based