11 minutes read

Dependency Injection (DI) is a design pattern that shifts the responsibility of creating and managing an object’s dependencies from the object itself to an external container—in this case, the Spring IoC (Inversion of Control) container. Instead of having a class manually create its dependencies, the container creates and injects them, leading to a decoupled, testable, and maintainable codebase.

The Spring IoC container creates and manages objects called beans. These beans are the building blocks of your application and can be declared using annotations such as @Component or methods annotated with @Bean inside configuration classes. DI is the process by which these beans are injected into one another to form a complete, working application.

A quick recap

Spring manages objects in two ways: beans declared explicitly with @Bean methods inside @Configuration classes, and components auto-detected via @Component (and its stereotypes @Service, @Repository, @Controller). Both are singletons by default, and both can be injected into each other with @Autowired. Dependency Injection is simply the mechanism that performs this wiring — Spring's IoC container builds the object graph so you never call new on a dependency yourself.

With that refresher in place, let's look at the three ways to write an injection point, and then how Spring resolves ambiguity when multiple beans of the same type exist.

Methods of dependency injection in Spring

Spring supports three common approaches to injecting dependencies.

  1. Field injection

You annotate a field directly with @Autowired, as you saw in the Spring components topic. It's quick to write but not recommended for production code — it makes testing harder and hides the dependency requirements.

  1. Setter injection

Setter-based injection uses a setter method marked with @Autowired to inject the dependency. This approach provides some flexibility but still doesn't offer immutability.

Java
@Component
public class Customer {
    private Product product;

    @Autowired
    public void setProduct(Product product) {
        this.product = product;
    }
}
Kotlin
@Component
class Customer {
    private lateinit var product: Product

    @Autowired
    fun setProduct(product: Product) {
        this.product = product
    }
}

Spring invokes the setter during object initialization. This gives more flexibility than field injection, but the code is less readable and harder to debug — and the field still can't be made final.

  1. Constructor injection (recommended)

Constructor-based injection is the most recommended approach, as you saw briefly in the Components topic. It makes dependencies explicit, enhances immutability (by allowing the dependency to be final), and simplifies testing.

Java
@Component
public class Customer {
    private final Product product;

    public Customer(Product product) {
        this.product = product;
    }
}
Kotlin
@Component
class Customer(val product: Product)

Advantages of constructor injection:

  • Testability: All dependencies are explicit and can be easily mocked.

  • Immutability: Dependencies can be declared as final, preventing reassignment.

  • Safety: It guarantees that required dependencies are provided at creation, reducing the chance of NullPointerExceptions.

So, which type of DI to use? According to official docs:

  • Constructor injection is the most recommended and preferable option for mandatory dependencies.

  • Setter injection should only be used for optional dependencies.

  • Field injection is to be avoided.

Type matching

Let's start by creating beans and adding them to the context.

First, we will create a normal class to be our bean:

Java
public class Car {

    private String name;
    private String model;

    public Car(String name, String model) {
        this.name = name;
        this.model = model;
    }

    //omitted getters and setters

}
Kotlin
class Car(var name: String, var model: String)

Now let's create the config class, which will be the container for our beans:

Java
@Configuration
public class Config {

    @Bean
    public Car teslaCar() {
        return new Car("Tesla", "2023");
    }

    @Bean
    public Car toyotaCar() {
        return new Car("Toyota", "2023");
    }
}
Kotlin
@Configuration
class Config {

    @Bean
    fun teslaCar(): Car {
        return Car("Tesla", "2023")
    }

    @Bean
    fun toyotaCar(): Car {
        return Car("Toyota", "2023")
    }
}

We have created two beans with the same type Car and annotated them with @Bean. Now let's print them out and see what we will get:

Java
public class DiApplication {
    public static void main(String[] args) {
        // Create the Spring application context
        var context = new AnnotationConfigApplicationContext(Config.class);
        // Retrieve an instance of MyBean
        Car myBean = context.getBean(Car.class);
        System.out.println(myBean.getName());

    }
}
Kotlin
@SpringBootApplication
class Di1Application

fun main(args: Array<String>) {
    runApplication<Di1Application>(*args)
    // Create the Spring application context
    val context = AnnotationConfigApplicationContext(Config::class.java)
    // Retrieve an instance of MyBean
    val myBean = context.getBean(Car::class.java)
    println(myBean.name)
}

Now run the application. BOOM! We get NoUniqueBeanDefinitionException. Why? Simply put, this happens because we are trying to get the bean by type and there are two beans of the same type in the context.

The first solution to this problem is to use @Primary, which tells Spring that if there are two or more beans of the same type, it should use the bean with this annotation. Now let's modify the Config class:

Java
@Configuration
public class Config {

    @Bean
    @Primary
    public Car teslaCar() {
        return new Car("Tesla", "2023");
    }
    
    @Bean
    public Car toyotaCar() {
        return new Car("Toyota", "2023");
    }
}
Kotlin
@Configuration
class Config {

    @Bean
    @Primary
    fun teslaCar(): Car {
        return Car("Tesla", "2023")
    }
    
    @Bean
    fun toyotaCar(): Car {
        return Car("Toyota", "2023")
    }
}

After running the code again, it should work and you should find "Tesla" in the output.

Another solution to this problem is matching the bean by name, so after removing @Primary from the bean, we can do the following:

Java
public class DiApplication {
    public static void main(String[] args) {
        // Create the Spring application context
        var context = new AnnotationConfigApplicationContext(Config.class);
        // Retrieve an instance of MyBean
        Car myBean = context.getBean("toyotaCar", Car.class);
        System.out.println(myBean.getName());
    }
}
Kotlin
@SpringBootApplication
class Di1Application

fun main(args: Array<String>) {
    runApplication<Di1Application>(*args)
    // Create the Spring application context
    val context = AnnotationConfigApplicationContext(Config::class.java)
    // Retrieve an instance of MyBean
    val myBean = context.getBean("toyotaCar", Car::class.java)
    println(myBean.name)
}

We added another argument to the getBean() method, which is responsible for matching the bean by the provided name. By default, the name is the method name. Now what if we want to change the name? We can just pass the name inside the @Bean annotation as follows:

Java
@Configuration
public class Config {

    @Bean("tesla")
    public Car teslaCar() {
        return new Car("Tesla", "2023");
    }

    @Bean("toyota")
    public Car toyotaCar() {
        return new Car("Toyota", "2023");
    }
}
Kotlin
@Configuration
class Config {

    @Bean("tesla")
    fun teslaCar(): Car {
        return Car("Tesla", "2023")
    }

    @Bean("toyota")
    fun toyotaCar(): Car {
        return Car("Toyota", "2023")
    }
}

That's it. We passed the name in the parentheses. Now let's retrieve it as we did in the previous example:

Java
public class DiApplication {
    public static void main(String[] args) {
        // Create the Spring application context
        var context = new AnnotationConfigApplicationContext(Config.class);
        // Retrieve an instance of MyBean
        Car myBean = context.getBean("tesla", Car.class);
        System.out.println(myBean.getName());
    }
}
Kotlin
@SpringBootApplication
class Di1Application

fun main(args: Array<String>) {
    runApplication<Di1Application>(*args)
    // Create the Spring application context
    val context = AnnotationConfigApplicationContext(Config::class.java)
    // Retrieve an instance of MyBean
    val myBean = context.getBean("tesla", Car::class.java)
    println(myBean.name)
}

The output will be "Tesla".

@Qualifier

You've already seen @Qualifier used on a @Bean method parameter back in the Spring beans topic. It works the same way on a field or constructor injection point — let's try it on a field.

Let's try injecting one of the Engine beans directly into Car:

Java
public class Car {

    private String name;
    private String model;

    @Autowired
    private Engine engine;

    public Car(String name, String model) {
        this.name = name;
        this.model = model;
    }

    //omitted getters and setters

}
Kotlin
class Car(
    var name: String,
    var model: String
) {
    @Autowired
    lateinit var engine: Engine
}

The compilation error suggests using @Qualifier, since there's more than one bean of type Engine. Let's update the code:

Java
public class Car {

    private String name;
    private String model;

    @Qualifier("teslaEngine")
    @Autowired
    private Engine engine;

    public Car(String name, String model) {
        this.name = name;
        this.model = model;
    }

    //omitted getters and setters

}
Kotlin
class Car(
    var name: String,
    var model: String,
    @Qualifier("teslaEngine") var engine: Engine
)

Now let's run this code:

Java
@SpringBootApplication
public class Di1Application {

    public static void main(String[] args) {

        var context = new AnnotationConfigApplicationContext(Config.class);
        var bean = context.getBean("tesla", Car.class);
        System.out.println(bean.getEngine().getBrand());
    }

}
Kotlin
@SpringBootApplication
class Di1Application

fun main(args: Array<String>) {
    runApplication<Di1Application>(*args)

    val context = AnnotationConfigApplicationContext(Config::class.java)
    val bean = context.getBean("tesla", Car::class.java)
    println(bean.engine.brand)
}

The output will be "Tesla" again.

Loose coupling

Let's create the following:

Java
public interface Engine {
    void start();
}
Kotlin
interface Engine {
    fun start()
}
Java
public class DieselEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Goes r-r-r-r... and exhausts black smoke");
    }
}
Kotlin
class DieselEngine : Engine {
    override fun start() {
        println("Goes r-r-r-r... and exhausts black smoke")
    }
}
Java
public class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("Goes b-z-z-z-z... and produces sparks");
    }
}
Kotlin
class ElectricEngine : Engine {
    override fun start() {
        println("Goes b-z-z-z-z... and produces sparks")
    }
}
Java
public class Vehicle {
    private Engine engine;

    public Vehicle(Engine engine) {
        this.engine = engine;
    }

    public void drive() {
        engine.start();
    }
}
Kotlin
class Vehicle(private val engine: Engine) {
    fun drive() {
        engine.start()
    }
}
Java
@Configuration
public class Config {
    @Bean
    public Engine dieselEngine() {
        return new DieselEngine();
    }

    @Bean
    public Engine electricEngine() {
        return new ElectricEngine();
    }


    @Bean
    public Vehicle vehicle(@Qualifier("electricEngine") Engine engine) {
        return new Vehicle(engine);
    }
}
Kotlin
@Configuration
class Config {
    @Bean
    fun dieselEngine(): Engine = DieselEngine()

    @Bean
    fun electricEngine(): Engine = ElectricEngine()

    @Bean
    fun vehicle(@Qualifier("electricEngine") engine: Engine): Vehicle = Vehicle(engine)
}

We created the interface Engine and two concrete classes that implement it, then injected Engine into Vehicle using constructor injection — this time resolving the two competing Engine beans with @Qualifier, exactly as before, but now injecting an interface type rather than a concrete class.

Now let's run it and see the results:

Java
@SpringBootApplication
public class DiApplication {

    public static void main(String[] args) {

        ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
        Vehicle vehicle = context.getBean(Vehicle.class);
        vehicle.drive();
    }

}
Kotlin
@SpringBootApplication
class DiApplication

fun main(args: Array<String>) {
    runApplication<Di1Application>(*args)

    val context: ApplicationContext = AnnotationConfigApplicationContext(Config::class.java)
    val vehicle: Vehicle = context.getBean(Vehicle::class.java)
    vehicle.drive()
}

The output:

// Goes b-z-z-z-z... and produces sparks

Conclusion

In this topic, you learned how to implement Spring in an application using different dependency injection methods setter-based, constructor, and field injection. Constructor injection is recommended for its clarity, safety, and simplicity. You also explored type matching and strategies to resolve ambiguity in Spring.

54 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo