Dependency Injection and IoC in Spring Boot

3 minute read

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, @Autowired is 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

  1. DI = objects receive dependencies, don’t create them.
  2. Use constructor injection — immutable, testable, explicit.
  3. @Qualifier picks a specific bean; @Primary sets the default.
  4. Singleton scope for 99% of beans — they should be stateless.
  5. DI makes testing easy — inject mocks via constructor.

What’s Next?

Next: @Bean, @Component, @Service, @Repository — stereotype annotations and when to use each.

Updated: