CDI and Spring DI are both powerful dependency injection frameworks in Java, but they approach the problem from fundamentally different philosophical standpoints.

Imagine you’re building a complex Java application. You have many components that need to collaborate, and you want to avoid the manual new operator and tightly coupled code. Dependency Injection (DI) is the answer. Both CDI (Contexts and Dependency Injection) and Spring DI provide this, but they offer distinct experiences.

Let’s see CDI in action with a simple example.

// MyService.java
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

@ApplicationScoped
public class MyService {

    private String message = "Hello from CDI!";

    public String getMessage() {
        return message;
    }
}
// MyConsumer.java
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

@ApplicationScoped
public class MyConsumer {

    @Inject
    private MyService myService;

    public void greet() {
        System.out.println(myService.getMessage());
    }
}
// MainApp.java (simulated execution context)
import javax.enterprise.inject.se.SeContainer;
import javax.enterprise.inject.se.SeContainerInitializer;

public class MainApp {
    public static void main(String[] args) {
        SeContainerInitializer initializer = SeContainerInitializer.newInstance();
        try (SeContainer container = initializer.initialize()) {
            MyConsumer consumer = container.select(MyConsumer.class).get();
            consumer.greet(); // Output: Hello from CDI!
        }
    }
}

In this snippet, @ApplicationScoped tells CDI that MyService and MyConsumer are singletons within the application’s lifecycle. The @Inject annotation is the magic: CDI sees that MyConsumer needs an instance of MyService and automatically provides one. No manual instantiation, no explicit wiring.

Now, let’s contrast this with Spring DI.

// MyService.java
import org.springframework.stereotype.Service;

@Service
public class MyService {

    private String message = "Hello from Spring!";

    public String getMessage() {
        return message;
    }
}
// MyConsumer.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MyConsumer {

    @Autowired
    private MyService myService;

    public void greet() {
        System.out.println(myService.getMessage());
    }
}
// MainApp.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan // Scans for @Component, @Service, etc.
public class MainAppConfig {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MainAppConfig.class);
        MyConsumer consumer = context.getBean(MyConsumer.class);
        consumer.greet(); // Output: Hello from Spring!
    }
}

Here, @Service and @Component are Spring’s stereotypes for managing beans. @Autowired is Spring’s mechanism for injecting dependencies. The AnnotationConfigApplicationContext bootstraps the Spring container, which scans for annotated components and wires them together.

The core problem both frameworks solve is managing object lifecycles and dependencies. Without DI, you’d have code like this:

public class BadConsumer {
    private MyService myService;

    public BadConsumer() {
        this.myService = new MyService(); // Tightly coupled!
    }

    public void greet() {
        System.out.println(myService.getMessage());
    }
}

This is brittle. If MyService’s constructor changes, BadConsumer breaks. If you want to use a different implementation of MyService (e.g., for testing), it’s a nightmare. DI decouples these concerns.

CDI is a specification within the Java EE (now Jakarta EE) ecosystem. It’s designed to be portable and integrated with other Java EE standards like JSF, EJB, and JAX-RS. Its core concepts revolve around:

  • Beans: Plain Old Java Objects (POJOs) that are managed by the CDI container.
  • Producers: Methods that create and provide instances of beans.
  • Observers: Methods that listen for and react to events fired by other beans.
  • Scopes: Define the lifecycle of a bean (e.g., @ApplicationScoped, @RequestScoped, @Dependent).
  • Qualifiers: Allow you to distinguish between multiple beans of the same type.

Spring DI, on the other hand, is part of the larger Spring Framework. While it excels at DI, its ecosystem extends far beyond that, offering solutions for transaction management, security, data access, web applications (Spring MVC/WebFlux), and more. Spring’s core concepts for DI include:

  • Beans: Objects managed by the Spring IoC (Inversion of Control) container.
  • IoC Container: The heart of Spring, responsible for creating, configuring, and managing beans.
  • Dependency Injection: Achieved through constructor injection, setter injection, or field injection.
  • AOP (Aspect-Oriented Programming): Tightly integrated with DI for cross-cutting concerns.
  • Configuration: Can be done via XML, Java annotations (@Configuration), or programmatic approaches.

A key difference lies in their philosophical leanings. CDI is often seen as more "standard" and less opinionated, aiming for interoperability across different Java EE implementations. Spring, while also offering flexibility, has historically been more opinionated, providing a comprehensive "one-stop shop" for enterprise Java development.

The way CDI handles context is particularly interesting. When you use @RequestScoped, CDI automatically manages the lifecycle of that bean for the duration of an HTTP request. If you inject a @RequestScoped bean into an @ApplicationScoped bean, CDI ensures that the application-scoped bean gets a proxy that delegates to the correct request-scoped instance for each request. This is handled transparently by the container.

If you’re building a Jakarta EE application and want a standard, specification-driven DI solution, CDI is the natural choice. If you’re working within the Spring ecosystem, or prefer a framework that offers a vast array of integrated features beyond DI, Spring DI is the way to go. The choice often boils down to the existing technology stack and project requirements.

The next hurdle you’ll likely encounter is understanding how to handle asynchronous operations and event handling within these DI frameworks.

Want structured learning?

Take the full Java course →