Understanding Dependency Injection and IoC in Spring Boot

Mohssine kissane
Software engineer
Why This Matters
You've probably written code like this before:
public class OrderService {
private EmailService emailService = new EmailService();
private PaymentService paymentService = new PaymentService();
public void processOrder(Order order) {
paymentService.process(order);
emailService.send(order.getCustomerEmail(), "Order confirmed");
}
}It works. But there's a problem: OrderService is tightly coupled to specific implementations. Testing becomes painful—you can't mock EmailService without changing the code. Switching to a different email provider? You'll need to modify OrderService itself.
This is exactly what Dependency Injection (DI) and Inversion of Control (IoC) solve.
What Is Inversion of Control?
Normally, your code controls the flow: "I need an EmailService, so I'll create one."
With IoC, you flip that: "I need an EmailService, but someone else will provide it to me."
That "someone else" is the Spring IoC container. Instead of creating dependencies yourself, you declare what you need, and Spring creates and manages them for you.
What Is Dependency Injection?
Dependency Injection is how IoC happens. It's the mechanism Spring uses to provide your objects with their dependencies.
Think of it like ordering room service. You don't go to the kitchen and cook. You declare what you need (room service), and it gets delivered to you. That's dependency injection.
How Spring Boot Does This
Spring Boot uses three types of injection:
1. Constructor Injection (Recommended)
@Service
public class OrderService {
private final EmailService emailService;
private final PaymentService paymentService;
public OrderService(EmailService emailService, PaymentService paymentService) {
this.emailService = emailService;
this.paymentService = paymentService;
}
public void processOrder(Order order) {
paymentService.process(order);
emailService.send(order.getCustomerEmail(), "Order confirmed");
}
}Spring sees the constructor and says, "You need EmailService and PaymentService? I'll find or create them and pass them in."
Why this is best: Your dependencies are final (immutable), required at construction time (no half-initialized objects), and testing is straightforward—just pass mock objects to the constructor.
2. Setter Injection
@Service public class OrderService { private EmailService emailService; @Autowired public void setEmailService(EmailService emailService) { this.emailService = emailService; } }When to use it: Rarely. Only for optional dependencies. Most dependencies should be required.
3. Field Injection
@Service public class OrderService { @Autowired private EmailService emailService; }Don't use this. It looks clean but makes testing harder (you can't easily inject mocks) and hides the complexity of your class (20 @Autowired fields? That's a code smell).
The Common Mistakes
Mistake #1: Creating dependencies manually
@Service public class OrderService { private EmailService emailService = new EmailService(); // Wrong! }If you create it yourself, Spring doesn't manage it. No DI, no magic.
Mistake #2: Forgetting @Component/@Service annotations
public class EmailService { // Spring doesn't know about this! public void send(String to, String message) { // ... } }Spring only manages beans—classes marked with @Component, @Service, @Repository, or @Controller. Without these annotations, Spring won't inject your class.
Correct approach:
@Service public class EmailService { public void send(String to, String message) { // ... } }How The Container Actually Works
When Spring Boot starts:
It scans your packages for classes with @Component, @Service, etc.
It creates instances of these classes (beans) and stores them in the IoC container
When a class needs a dependency, Spring looks in its container: "Do I have an EmailService bean? Yes. Let me inject it."
This happens once at startup. Your beans are typically singletons—one instance shared across your application.
The Real Power: Testing
Without DI:
@Test public void testOrderProcessing() { OrderService service = new OrderService(); // How do I prevent real emails from being sent? I can't. service.processOrder(order); }With DI:
@Test public void testOrderProcessing() { EmailService mockEmail = mock(EmailService.class); PaymentService mockPayment = mock(PaymentService.class); OrderService service = new OrderService(mockEmail, mockPayment); service.processOrder(order); verify(mockEmail).send(anyString(), anyString()); }You control the dependencies. Testing becomes simple.
Your Next Step
Look at your current Spring Boot project. Find a service class. Ask yourself:
- Is it using constructor injection?
- Are the dependencies final?
- Could you test this class by passing mock objects to the constructor?
If you answered "no" to any of these, refactor to constructor injection. Your future self (and your test suite) will thank you.
Remember: Don't create dependencies. Declare them. Let Spring do the heavy lifting. That's the entire point of IoC and DI—writing loosely coupled, testable code without the boilerplate.