Spring Boot Dependency Injection Tutorial: Learn with Real-World scenarios

How can you reduce coupling, improve cohesion, and increase testability in your Spring Boot applications? The answer is dependency injection. In this tutorial, you will learn how to use it with practical scenarios and examples.

Eidan Khan
Make Android
9 min readJan 8, 2024

--

Dependency injection is a technique that allows objects to receive their dependencies from an external source, rather than creating or managing them themselves. Dependencies are the other objects that an object needs to perform its tasks. For example, a `Car` object may depend on a `Engine` object to run, and a `Engine` object may depend on a `Fuel` object to function.

Image generated by DALL-E 3

By using dependency injection, we can achieve several benefits, such as:

  • Loose coupling: Objects are not tightly bound to their dependencies and can work with different implementations of the same interface.
  • Testability: Objects can be easily tested by injecting mock or stub dependencies, without changing the code.
  • Reusability: Objects can be reused in different contexts, as they do not depend on specific details of their dependencies.
  • Maintainability: Objects can be easily modified or replaced, as they do not create or manage their dependencies.

The main objective of this article is to help you learn how to use dependency injection in Spring Boot with practical examples. We will cover the following topics:

  • Types of dependency injection in Spring Boot: constructor, setter, and field injection
  • Practical examples of using dependency injection in Spring Boot for different scenarios
  • Tips and best practices for using dependency injection in Spring Boot

By the end of this article, you should be able to understand and apply dependency injection in Spring Boot effectively and efficiently. Let’s get started!

But before we dive into the details, you might want to check out my previous article, where I show you how to supercharge your Spring Boot app with 3 proven optimization techniques?

Types of Dependency Injection

There are three types of dependency injection (DI) in Spring Boot: constructor injection, setter injection, and field injection. Each type has its own advantages and disadvantages, and can be used for different scenarios. In this section, we will explain each type and provide some code snippets to demonstrate how to use them in Spring Boot.

Constructor Injection:

It is the most recommended type of DI in Spring Boot. It is accomplished by the container invoking a constructor with a number of arguments, each representing a dependency. This way, the dependencies are injected when the bean is created, and the bean becomes immutable and ready to use.

The advantages of constructor injection are:

  • It ensures that the bean is fully initialized and has all the required dependencies.
  • It prevents circular dependencies, as the dependencies are resolved before the bean is created.
  • It makes the code more readable and testable, as the dependencies are explicitly declared in the constructor.

The disadvantages of constructor injection are:

  • It can make the constructor too large and complex, especially if the bean has many dependencies.
  • It can create a lot of boilerplate code, especially if the bean has many constructor arguments.

To use constructor injection in Spring Boot, we need to annotate the constructor with the @Autowired annotation, and optionally the @Qualifier annotation to specify the bean name if there are multiple candidates for the same dependency type. For example, suppose we have a `Car` class that depends on an `Engine` interface, and two implementations of the `Engine` interface: `PetrolEngine` and `DieselEngine`. We can use constructor injection to inject the desired `Engine` implementation into the `Car` class as follows:

@Component
public class Car {

private final Engine engine;

@Autowired
public Car(@Qualifier("dieselEngine") Engine engine) {
this.engine = engine;
}

public void start() {
engine.start();
}
}

@Component("petrolEngine")
public class PetrolEngine implements Engine {

@Override
public void start() {
System.out.println("Starting petrol engine...");
}
}

@Component("dieselEngine")
public class DieselEngine implements Engine {

@Override
public void start() {
System.out.println("Starting diesel engine...");
}
}

In this example, we use the @Qualifier annotation to inject the dieselEngine bean into the Car constructor, and the @Component annotation to specify the bean names for the Engine implementations. Alternatively, we can use the @Primary annotation to mark one of the Engine implementations as the primary candidate for injection, and omit the @Qualifier annotation in the Car constructor.

Setter Injection:

Setter injection is another type of DI in Spring Boot. It is accomplished by the container invoking setter methods on the bean after it is constructed, and passing the dependencies as arguments. This way, the dependencies are injected after the bean is created, and the bean becomes mutable and configurable.

The advantages of setter injection are:

  • It allows for optional and dynamic dependencies, as the setter methods can be called or not called depending on the situation.
  • It allows for changing the dependencies at runtime, as the setter methods can be called again with different arguments.

The disadvantages of setter injection are:

  • It does not ensure that the bean is fully initialized and has all the required dependencies, as the setter methods may not be called or may be called in the wrong order.
  • It creates circular dependencies, as the dependencies are resolved after the bean is created.
  • It makes the code less readable and testable, as the dependencies are hidden in the setter methods.

To use setter injection in Spring Boot, we need to annotate the setter methods with the @Autowired annotation, and optionally the @Qualifier annotation to specify the bean name if there are multiple candidates for the same dependency type. For example, suppose we have a Student class that depends on a Course interface, and two implementations of the Course interface: MathCourse and ScienceCourse. We can use setter injection to inject the desired Course implementation into the Student class as follows:

@Component
public class Student {

private Course course;

@Autowired
public void setCourse(Course course) {
this.course = course;
}

public void study() {
course.study();
}
}

@Component("mathCourse")
public class MathCourse implements Course {

@Override
public void study() {
System.out.println("Studying math...");
}
}

@Component("scienceCourse")
@Primary
public class ScienceCourse implements Course {

@Override
public void study() {
System.out.println("Studying science...");
}
}

In this example, we use the @Primary annotation to mark the scienceCourse bean as the primary candidate for injection, and remove the @Qualifier annotation from the Student setter method. This way, the scienceCourse bean will be injected into the Student class by default, unless another Course implementation is specified explicitly.

Field Injection:

Field injection is the least recommended type of DI in Spring Boot. It is accomplished by the container injecting the dependencies directly into the fields of the bean, without using constructors or setter methods. This way, the dependencies are injected after the bean is created, and the bean becomes mutable and configurable.

The advantages of field injection are:

  • It reduces the amount of boilerplate code, as there is no need to write constructors or setter methods.
  • It allows for optional and dynamic dependencies, as the fields can be annotated with the @Autowired annotation and the required attribute.

The disadvantages of field injection are:

  • It does not ensure that the bean is fully initialized and has all the required dependencies, as the fields may not be injected or may be injected in the wrong order.
  • It creates circular dependencies, as the dependencies are resolved after the bean is created.
  • It makes the code less readable and testable, as the dependencies are hidden in the fields and cannot be accessed or mocked easily.

To use field injection in Spring Boot, we need to annotate the fields with the @Autowired annotation, and optionally the @Qualifier annotation to specify the bean name if there are multiple candidates for the same dependency type. For example, suppose we have a Book class that depends on an Author interface, and two implementations of the Author interface: FictionAuthor and NonFictionAuthor. We can use field injection to inject the desired Author implementation into the Book class as follows:

@Component
public class Book {

@Autowired
@Qualifier("fictionAuthor")
private Author author;

public void read() {
author.read();
}
}

@Component("fictionAuthor")
public class FictionAuthor implements Author {

@Override
public void read() {
System.out.println("Reading fiction...");
}
}

@Component("nonFictionAuthor")
public class NonFictionAuthor implements Author {

@Override
public void read() {
System.out.println("Reading non-fiction...");
}
}

Practical Examples

In this section, we will see some practical examples of using dependency injection in Spring Boot for different scenarios. We will show how to use annotations, interfaces, qualifiers, and profiles, to configure and manage dependencies.

Annotations:

You can use annotations such as @Component, @Service, @Repository, and @Controller to mark your classes as Spring beans and make them eligible for dependency injection. These annotations indicate the role of the class in the application and also enable component scanning, which means that Spring will automatically detect and register these beans in the application context.

For example, suppose you have a class called UserService that provides some business logic for user management. You can annotate it with @Service to make it a Spring bean:

@Service
public class UserService {
// some business logic methods
}

Then, you can inject this bean into another class, such as a controller, using the @Autowired annotation:

@Controller
public class UserController {

@Autowired
private UserService userService;

// some controller methods
}

The @Autowired annotation tells Spring to inject the UserService bean into the UserController class. Spring will look for a bean of type UserService in the application context and inject it into the UserController instance. If there is more than one bean of the same type, you can use the @Qualifier annotation to specify which bean to inject by name.

Interfaces:

You can also use interfaces to define the contracts for your dependencies and implement them in different classes. This way, you can decouple your components from the concrete implementations of their dependencies and make them more flexible and testable.

For example, suppose you have an interface called UserRepository that defines the methods for accessing user data from a data source. You can implement this interface in different classes, such as JdbcUserRepository, MongoUserRepository, and InMemoryUserRepository, to provide different data access strategies:

public interface UserRepository {

// some data access methods
}

@Repository
public class JdbcUserRepository implements UserRepository {

// some JDBC implementation
}

@Repository
public class MongoUserRepository implements UserRepository {

// some MongoDB implementation
}

@Repository
public class InMemoryUserRepository implements UserRepository {

// some in-memory implementation
}

Then, you can inject the UserRepository interface into your UserService class and let Spring choose the appropriate implementation based on the configuration:

@Service
public class UserService {

@Autowired
private UserRepository userRepository;

// some business logic methods
}

Spring will inject the UserRepository bean that matches the active profile, which is a way to group and activate different beans based on the environment. For example, you can define a profile called “dev” and annotate the InMemoryUserRepository bean with @Profile(“dev”) to indicate that it should be used only in the development environment. Similarly, you can define other profiles for other environments and beans.

Qualifiers

As we mentioned before, you can use the @Qualifier annotation to specify which bean to inject by name when there are multiple beans of the same type. This annotation can be used along with the @Autowired annotation to resolve the ambiguity.

For example, suppose you have two beans of type DataSource, one for the primary database and one for the secondary database. You can name them using the @Qualifier annotation:

@Configuration
public class DataSourceConfig {

@Bean
@Qualifier("primaryDataSource")
public DataSource primaryDataSource() {
// some primary data source configuration
}

@Bean
@Qualifier("secondaryDataSource")
public DataSource secondaryDataSource() {
// some secondary data source configuration
}
}

Then, you can inject them into your data access classes using the @Qualifier annotation:

@Repository
public class PrimaryUserRepository implements UserRepository {

@Autowired
@Qualifier("primaryDataSource")
private DataSource dataSource;

// some data access methods
}

The @Qualifier annotation tells Spring to inject the DataSource bean that matches the given name. This way, you can avoid conflicts and confusion when injecting multiple beans of the same type.

Profiles

As we mentioned before, you can use the @Profile annotation to group and activate different beans based on the environment. This annotation can be used along with the @Component, @Service, @Repository, and @Controller annotations to mark the beans that belong to a specific profile.

For example, suppose you have two beans of type MailSender, one for the production environment and one for the testing environment. You can annotate them with the @Profile annotation:

@Component
@Profile("prod")
public class SmtpMailSender implements MailSender {

// some SMTP implementation
}

@Component
@Profile("test")
public class MockMailSender implements MailSender {

// some mock implementation
}

Then, you can inject the MailSender interface into your notification service and let Spring choose the appropriate implementation based on the active profile:

@Service
public class NotificationService {

@Autowired
private MailSender mailSender;

// some notification methods
}

Spring will inject the MailSender bean that matches the active profile, which can be set using the spring.profiles.active property in the application.properties file or as a command-line argument. For example, you can set the active profile to “test” by adding the following line to the application.properties file:

spring.profiles.active=test

Or by passing the following argument to the Java command:

java -jar -Dspring.profiles.active=test app.jar

In this article, we have learned about dependency injection in Spring Boot and how it can help us to create loosely coupled, testable, and maintainable applications. We have seen some practical examples of using dependency injection in Spring Boot for different scenarios and how to use annotations, interfaces, qualifiers, and profiles, to configure and manage dependencies.

I hope you have enjoyed reading this article and learned something new and useful from it. Writing content requires a lot of effort and research, so I appreciate your time and attention. If you liked this article, please support me by following, sharing, and commenting on it. Your feedback and suggestions are very valuable to me and help me improve my writing skills. Thank you for your support and encouragement. 😊

--

--

Eidan Khan
Make Android

🚀 Full-stack Dev | Tech Content Creator 📝 For more in-depth articles, tutorials, and insights, visit my blog at JavaJams.org.