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.
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.
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.
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 sparksConclusion
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.