Dependency Injection (dependency injection) sounds fancy, but it's just a trendy name for passing one object (a dependency) into another object. Put simply, DI is the way an object (let's call it the consumer) gets all the dependencies it needs to do its job.
Think of a restaurant. Cooking dishes is the chef's responsibility. Of course, the chef won't run to the market hunting for the best veggies, nor will they raise cows for the beef steak. Other specialists handle that, and the restaurant provides the chef with the ingredients. In programming it's similar: an object shouldn't create its own dependencies — that should be done from the outside.
Dependency injection (DI) makes your code:
- Flexible: if you need to swap a dependency (for example, switch from one database to another), you can do it without rewriting all the business logic.
- Testable: you can easily replace dependencies with stubs/mocks for testing.
- Easy to maintain: changes in one dependency minimally affect the rest of the code.
The problem with the traditional approach
Let's look at an example without DI:
public class OrderService {
private final PaymentService paymentService;
public OrderService() {
this.paymentService = new PaymentService(); // We create the dependency instance ourselves
}
public void placeOrder() {
paymentService.processPayment();
}
}
What's wrong here? Well, at least:
OrderServicedepends on a concrete implementation ofPaymentService. If we need to use a differentPaymentService, we'd have to change the code.- Testing
OrderServicebecomes harder because we can't replacePaymentServicewith a stub.
How DI fixes it
DI solves this by passing dependencies from the outside:
public class OrderService {
private final PaymentService paymentService;
// Dependency is passed in from outside via the constructor
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.processPayment();
}
}
Now we can pass any implementation of PaymentService (for example, CreditCardPaymentService or PayPalPaymentService), which makes the code flexible.
Benefits of using DI
Testability
With DI you can easily replace real dependencies with stubs/mocks for testing. For example:
PaymentService mockPaymentService = Mockito.mock(PaymentService.class);
OrderService orderService = new OrderService(mockPaymentService);
Maintainability
When objects don't create their own dependencies, it's easier to swap them later. For example, if you need to add a new implementation of PaymentService, you can do it without changing OrderService.
Scalability
DI helps scale applications. Imagine if instead of 10 classes you had 1000, and all of them manually created their own dependencies. Managing that turns into a nightmare. DI handles this easily and frees you from dependency management chores.
Ways to inject dependencies
Spring Framework supports three main DI approaches:
- Constructor-based
- Setter-based
- Field-based
We'll dig into them in the next lecture, but here's a quick overview.
Constructor injection
With this approach dependencies are passed through the constructor parameters:
@Component
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.processPayment();
}
}
In most cases it's best to use this approach because it:
- Makes dependencies required to initialize (the compiler helps enforce the constructor).
- Allows creating immutable dependencies (
final).
Setter injection
Dependencies are passed via setters. This makes them optional:
@Component
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
public void placeOrder() {
paymentService.processPayment();
}
}
This approach is better for default configurations or situations where dependencies aren't always required.
Field injection
This way uses the @Autowired annotation directly on the field. While it's the most concise, it's less preferred because it breaks encapsulation:
@Component
public class OrderService {
@Autowired
private PaymentService paymentService;
public void placeOrder() {
paymentService.processPayment();
}
}
DI in Spring Framework
So, Spring Framework gives us tools to use DI. That's great — we no longer have to manage dependencies by hand. The main DI principles in Spring:
Automatic dependency wiring (Autowired)
The @Autowired annotation is used to automatically inject required dependencies. Spring figures out the appropriate dependency by type.
Example:
@Component
public class OrderService {
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
Spring will automatically find a bean of type PaymentService and pass it into the constructor.
Java-based configuration with the @Bean annotation
You can also define dependencies manually using configuration classes:
@Configuration
public class AppConfig {
@Bean
public PaymentService paymentService() {
return new PayPalPaymentService();
}
@Bean
public OrderService orderService(PaymentService paymentService) {
return new OrderService(paymentService);
}
}
DI in real projects
We could repeat this one more time, but you probably already know DI helps a lot in web apps, microservices, and testing.
- Microservices: in microservices DI is used to inject services, repositories, configurations, and API clients.
- Web applications: DI lets you keep a single instance of a service that's used by all controllers to handle requests.
- Testing: DI makes it easy to swap real dependencies with mock objects when writing tests.
Real example
If you're building a REST API to handle orders, DI makes it easy to wire controllers, services, and repositories:
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
@Autowired
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<String> createOrder() {
orderService.placeOrder();
return ResponseEntity.ok("Order placed successfully!");
}
}
Here OrderController is totally agnostic — it doesn't care how OrderService is implemented; it just uses it.
Common mistakes and how to fix them
Circular dependencies
If two beans reference each other, Spring won't be able to inject them and will throw an error. For example:
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
private A a;
}
To fix this, it's better to rethink the app architecture and make dependencies one-way.
Ambiguous dependencies
If you have multiple beans of the same type, Spring doesn't know which one to inject:
@Component
public class CreditCardPaymentService implements PaymentService {}
@Component
public class PayPalPaymentService implements PaymentService {}
@Component
public class OrderService {
@Autowired
private PaymentService paymentService; // Error: which bean to choose?
}
The solution is to use the @Qualifier annotation:
@Component
public class OrderService {
@Autowired
@Qualifier("creditCardPaymentService")
private PaymentService paymentService;
}
Now that you understand what DI is, why it's useful, and how to apply it, we're ready to dive into the different ways to inject dependencies so you can pick the right tool for each situation.
GO TO FULL VERSION