Dependency Injection and IoC in Spring Boot
Dependency Injection (DI) is the single most important concept in Spring. Every Spring feature — controllers, services, security, data access — is built on it.
The Problem DI Solves
// ❌ WITHOUT DI — tightly coupled
public class BookService {
private BookRepository repo = new BookRepository(); // creates its own dependency
private EmailService email = new EmailService(); // creates its own dependency
// How do you test this with a fake repository?
// How do you switch to a different email provider?
}
// ✅ WITH DI — loosely coupled
public class BookService {
private final BookRepository repo;
private final EmailService email;
public BookService(BookRepository repo, EmailService email) {
this.repo = repo; // dependencies INJECTED from outside
this.email = email;
}
}
IoC (Inversion of Control): The object doesn’t create its dependencies — the container (Spring) creates and injects them.
Constructor Injection (Preferred)
@Service
public class BookService {
private final BookRepository bookRepository;
private final NotificationService notificationService;
// Spring automatically injects both dependencies
public BookService(BookRepository bookRepository,
NotificationService notificationService) {
this.bookRepository = bookRepository;
this.notificationService = notificationService;
}
}
When a class has one constructor,
@Autowiredis optional — Spring injects automatically.
Why Constructor Injection?
| Benefit | Explanation |
|---|---|
| Immutable | Fields can be final |
| Required deps | App won’t start if a dependency is missing |
| Testable | Easy to pass mocks in tests |
| No reflection | Unlike field injection |
Field Injection (Discouraged)
@Service
public class BookService {
@Autowired
private BookRepository bookRepository; // ❌ Not recommended
@Autowired
private NotificationService notificationService; // ❌ Can't be final
}
Problems: can’t make fields final, harder to test, hides dependencies.
Setter Injection (Rare)
@Service
public class BookService {
private BookRepository bookRepository;
@Autowired
public void setBookRepository(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
Use setter injection only for optional dependencies.
@Qualifier — Choosing Between Multiple Beans
public interface NotificationService {
void send(String message);
}
@Service("emailNotification")
public class EmailNotificationService implements NotificationService {
public void send(String message) { /* send email */ }
}
@Service("smsNotification")
public class SmsNotificationService implements NotificationService {
public void send(String message) { /* send SMS */ }
}
// Which one to inject? Use @Qualifier
@Service
public class BookService {
private final NotificationService notificationService;
public BookService(@Qualifier("emailNotification") NotificationService notificationService) {
this.notificationService = notificationService;
}
}
@Primary — Default Bean
@Primary
@Service("emailNotification")
public class EmailNotificationService implements NotificationService {
// This will be injected by default when no @Qualifier is specified
}
@Service("smsNotification")
public class SmsNotificationService implements NotificationService { }
@Primary wins when there’s ambiguity. @Qualifier overrides @Primary.
Bean Scopes
@Service
@Scope("singleton") // default — one instance for the entire app
public class BookService { }
@Component
@Scope("prototype") // new instance every time it's requested
public class ShoppingCart { }
@Component
@Scope("request") // one instance per HTTP request (web only)
public class RequestContext { }
@Component
@Scope("session") // one instance per HTTP session (web only)
public class UserSession { }
| Scope | Instances | Use Case |
|---|---|---|
| singleton (default) | 1 per app | Services, repositories, configs |
| prototype | New each time | Stateful objects |
| request | 1 per HTTP request | Request-scoped data |
| session | 1 per HTTP session | User session data |
99% of beans should be singleton — stateless services, repositories, controllers.
Testing with DI
DI makes unit testing trivial:
@ExtendWith(MockitoExtension.class)
class BookServiceTest {
@Mock
private BookRepository bookRepository;
@Mock
private NotificationService notificationService;
@InjectMocks
private BookService bookService;
@Test
void shouldCreateBook() {
// Dependencies are mocked — no database, no email
when(bookRepository.save(any())).thenReturn(new Book(1L, "Clean Code"));
Book result = bookService.createBook("Clean Code", "Robert Martin");
assertNotNull(result);
verify(notificationService).send(anyString());
}
}
Common Mistakes
| Mistake | Fix |
|---|---|
| Using field injection | Use constructor injection |
| Circular dependencies | Refactor — extract shared logic into a new class |
Creating beans with new |
Let Spring create them — use DI |
| Injecting into static fields | Spring can’t inject statics — redesign |
Key Takeaways
- DI = objects receive dependencies, don’t create them.
- Use constructor injection — immutable, testable, explicit.
@Qualifierpicks a specific bean;@Primarysets the default.- Singleton scope for 99% of beans — they should be stateless.
- DI makes testing easy — inject mocks via constructor.
What’s Next?
Next: @Bean, @Component, @Service, @Repository — stereotype annotations and when to use each.