Skillversitas

Dependency Injection

What Is Dependency Injection?

Dependency Injection (DI) is a pattern in which an object does not create its own dependencies; instead, they are provided (injected) by an external party — in this case, the Spring IoC container.

// Tight Coupling — the class creates its own dependencies
public class OrderService {
    private final PaymentService payment = new PaymentService(); // ← problem!
    private final NotificationService notif = new NotificationService();
}

// Loose Coupling — dependencies are injected from outside
public class OrderService {
    private final PaymentService payment;
    private final NotificationService notif;

    public OrderService(PaymentService payment, NotificationService notif) {
        this.payment = payment;
        this.notif = notif;
    }
}

IoC (Inversion of Control)

IoC is the underlying principle: the program's control flow is inverted from the class to the framework. Instead of the class managing its dependencies' lifecycle, Spring manages it.

Without IoC:  Class → creates → dependency
With IoC:     Container → injects → into Class

@Autowired

The @Autowired annotation tells Spring to automatically inject a bean.

@Service
public class UserService {

    private final UserRepository userRepository;

    @Autowired  // Can be omitted if there is only one constructor
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

Since Spring 4.3, @Autowired is not required if the class has only one constructor.

Setter Injection

@Service
public class UserService {

    private UserRepository userRepository;

    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}
@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;  // ← avoid!
}
Method Immutability Testable Recommended
Constructor ✅ Final field ✅ Easy to mock ✅ Yes
Setter ❌ Can be null ⚠️ Needs setup ⚠️ Sometimes
Field ❌ Can be null ❌ Hard to mock ❌ No

@Qualifier

When there is more than one bean of the same type, use @Qualifier to pick one:

@Component("emailNotifier")
public class EmailNotifier implements Notifier {
    // ...
}

@Component("smsNotifier")
public class SmsNotifier implements Notifier {
    // ...
}

@Service
public class NotificationService {

    private final Notifier notifier;

    public NotificationService(@Qualifier("emailNotifier") Notifier notifier) {
        this.notifier = notifier;
    }
}

@Primary

Mark a bean as the default when multiple beans of the same type exist:

@Component
@Primary
public class EmailNotifier implements Notifier {
    // ...
}

@Component
public class SmsNotifier implements Notifier {
    // ...
}
// NotificationService will get EmailNotifier by default

Loose Coupling vs Tight Coupling

Aspect Tight Coupling Loose Coupling
Dependency Created by itself Injected
Testing Hard Easy to mock
Swapping implementation Change the class code Change the configuration
Example new PaymentService() Constructor injection

Best Practices

  1. Use constructor injection — immutable, testable, dependencies are clear
  2. Avoid field injection — hard to test, not immutable
  3. Use interfaces for abstraction — swap the implementation without changing the caller
  4. Don't use new to create dependencies managed by Spring

With dependency injection, your application becomes loosely coupled and easy to test. In the next chapter, we'll understand Bean Lifecycle & Scopes — how Spring creates, manages, and destroys beans.