Skip to main content

Chapter 2: Dependency Injection (DI)

Introduction​

In Chapter 1, you understood WHY IoC exists. Now, you will learn HOW Spring injects dependencies.

What You Will Learn​

  1. The 3 types of dependency injection
  2. Why constructor injection is preferred
  3. How to use @Autowired, @Inject, @Resource
  4. Managing optional dependencies

Duration: 45 minutes


The 3 Types of Injection​

Spring offers 3 ways to inject dependencies:

  1. Constructor Injection ✅ (Recommended)
  2. Setter Injection
  3. Field Injection ⚠️ (Discouraged)

Let's look at each in detail.


1. Constructor Injection ✅​

Syntax​

@Service
public class OrderService {

// Dependencies declared as 'final'
private final EmailService emailService;
private final PaymentService paymentService;

// Injection via constructor
public OrderService(EmailService emailService,
PaymentService paymentService) {
this.emailService = emailService;
this.paymentService = paymentService;
}
}

How Does It Work?​

At application startup:

1. Spring finds OrderService
↓
2. Spring sees the constructor with 2 parameters
↓
3. Spring creates EmailService
↓
4. Spring creates PaymentService
↓
5. Spring creates OrderService by passing the 2 dependencies
↓
6. OrderService is ready!

Advantages of Constructor Injection​

Advantage 1: Immutability​

private final EmailService emailService;
// ^^^^^
// Cannot be modified after creation!

Why is this good?

  • ✅ Thread-safe (no concurrency issues)
  • ✅ Predictable (cannot change)
  • ✅ Fewer bugs

Advantage 2: Mandatory Dependencies​

public OrderService(EmailService emailService) {
this.emailService = emailService;
}

If Spring cannot find EmailService:

❌ Application refuses to start!
❌ Clear error at startup

That's GOOD! You discover the problem immediately, not in production!


Advantage 3: Easy to Test​

@Test
public void testOrderService() {
// Create fakes
EmailService fakeEmail = new FakeEmailService();
PaymentService fakePayment = new FakePaymentService();

// Easily inject via constructor
OrderService service = new OrderService(fakeEmail, fakePayment);

// Test
service.processOrder(new Order());
}

No need for Spring to test!


Advantage 4: No Need for @Autowired​

Since Spring 4.3, @Autowired is optional on constructors:

@Service
public class OrderService {
private final EmailService emailService;

// No need for @Autowired!
public OrderService(EmailService emailService) {
this.emailService = emailService;
}
}

Spring automatically detects the constructor!


Summary: Constructor Injection​

✅ Immutable dependencies (final)
✅ Mandatory dependencies
✅ Easy to test
✅ No need for @Autowired
✅ Recommended by Spring

👉 ALWAYS USE CONSTRUCTOR INJECTION!

2. Setter Injection​

Syntax​

@Service
public class OrderService {

// NON-final dependency
private EmailService emailService;

// Injection via a setter
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
}

How Does It Work?​

1. Spring creates OrderService (empty constructor)
↓
2. Spring creates EmailService
↓
3. Spring calls setEmailService(emailService)
↓
4. OrderService is ready

When to Use?​

Use case: Optional dependencies

@Service
public class OrderService {

private EmailService emailService;
private SmsService smsService; // Optional

@Autowired(required = false) // ← Optional!
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}

public void notifyCustomer(String message) {
// Email always sent
emailService.send(message);

// SMS only if available
if (smsService != null) {
smsService.send(message);
}
}
}

Disadvantages​

❌ Mutable dependencies (not 'final')
❌ Can be null (risk of NullPointerException)
❌ Object created in an incomplete state
❌ More difficult to test

Use only for optional dependencies!


3. Field Injection ⚠️​

Syntax​

@Service
public class OrderService {

// Direct injection into the field
@Autowired
private EmailService emailService;

@Autowired
private PaymentService paymentService;
}

How Does It Work?​

1. Spring creates OrderService
↓
2. Spring uses REFLECTION to access private fields
↓
3. Spring injects dependencies directly
↓
4. OrderService is ready

Why Is It Discouraged?​

Problem 1: Cannot Make 'final'​

@Autowired
private EmailService emailService; // ❌ Cannot be 'final'

Problem 2: Difficult to Test​

@Test
public void testOrderService() {
OrderService service = new OrderService();

// ❌ How to inject dependencies?
// Fields are private!

// Need to use reflection... complicated!
}

Problem 3: Hidden Dependencies​

@Service
public class OrderService {
@Autowired
private Dep1 dep1;
@Autowired
private Dep2 dep2;
@Autowired
private Dep3 dep3;
@Autowired
private Dep4 dep4;
@Autowired
private Dep5 dep5;
// ... 10 dependencies!
}

Problem: It's not easy to see all dependencies!

With constructor:

public OrderService(Dep1 d1, Dep2 d2, Dep3 d3, Dep4 d4, Dep5 d5) {
// ❌ WOW! Too many dependencies!
// ➡️ Signal that the class does too much
}

Summary: Field Injection​

❌ Not immutable
❌ Difficult to test
❌ Hides design issues
❌ Uses reflection (slower)

👉 AVOID FIELD INJECTION!

Comparison of the 3 Types​

CriteriaConstructorSetterField
Immutability (final)✅ Yes❌ No❌ No
Mandatory dependencies✅ Yes❌ No⚠️ Not clear
Easy to test✅ Yes⚠️ Medium❌ No
Readability✅ Clear✅ Clear⚠️ Hidden
@Autowired needed❌ No✅ Yes✅ Yes
Recommendation✅ USE⚠️ Optional❌ AVOID

Injection Annotations​

Spring supports 3 annotations for injection:

1. @Autowired (Spring)​

@Service
public class OrderService {

private final EmailService emailService;

@Autowired // Spring annotation
public OrderService(EmailService emailService) {
this.emailService = emailService;
}
}

Origin: Spring Framework
Usage: Most common


2. @Inject (Java Standard)​

import jakarta.inject.Inject;

@Service
public class OrderService {

private final EmailService emailService;

@Inject // Jakarta annotation (Java standard)
public OrderService(EmailService emailService) {
this.emailService = emailService;
}
}

Origin: Jakarta EE (formerly Java EE)
Usage: If you want portable code (not tied to Spring)


3. @Resource (Java Standard)​

import jakarta.annotation.Resource;

@Service
public class OrderService {

@Resource(name = "emailService") // Injection by name
private EmailService emailService;
}

Origin: Jakarta EE
Difference: Injects by name instead of type


Which One to Choose?​

Recommendation: @Autowired (or none for constructor)

Why?
✅ Spring standard
✅ Optional on constructor
✅ More options (required, @Qualifier)
✅ Better documented

Handling Multiple Implementations​

Problem​

public interface EmailService {
void send(String to, String message);
}

@Service
public class GmailService implements EmailService { ... }

@Service
public class SendGridService implements EmailService { ... }

// ❌ Spring doesn't know which one to choose!

Solution 1: @Primary​

@Service
@Primary // ← This is the default implementation
public class GmailService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("Sending via Gmail to " + to);
}
}

@Service
public class SendGridService implements EmailService {
@Override
public void send(String to, String message) {
System.out.println("Sending via SendGrid to " + to);
}
}

Spring will use GmailService by default!


Solution 2: @Qualifier​

@Service
public class OrderService {

private final EmailService emailService;

public OrderService(@Qualifier("sendGridService") EmailService emailService) {
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// Specific bean name
this.emailService = emailService;
}
}

Spring will specifically use SendGridService!


Solution 3: Explicit Naming​

@Service("gmailEmailService")  // ← Explicit name
public class GmailService implements EmailService { ... }

@Service("sendGridEmailService") // ← Explicit name
public class SendGridService implements EmailService { ... }

// Usage
public OrderService(@Qualifier("sendGridEmailService") EmailService service) {
this.emailService = service;
}

Optional Dependencies​

With @Autowired(required = false)​

@Service
public class NotificationService {

private final EmailService emailService;
private SmsService smsService; // Optional

// Mandatory
public NotificationService(EmailService emailService) {
this.emailService = emailService;
}

// Optional
@Autowired(required = false)
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}

public void notify(String message) {
emailService.send(message); // Always

if (smsService != null) {
smsService.send(message); // If available
}
}
}

With Optional<T>​

import java.util.Optional;

@Service
public class NotificationService {

private final EmailService emailService;
private final Optional<SmsService> smsService;

public NotificationService(
EmailService emailService,
Optional<SmsService> smsService // ← Optional wrapper
) {
this.emailService = emailService;
this.smsService = smsService;
}

public void notify(String message) {
emailService.send(message);

smsService.ifPresent(sms -> sms.send(message));
// ^^^^^^^^^
// Called only if present
}
}

Complete Example​

// Interface
public interface PaymentProcessor {
void process(Order order);
}

// Implementation 1
@Service
@Primary // ← Default
public class StripePaymentProcessor implements PaymentProcessor {

@Override
public void process(Order order) {
System.out.println("Payment via Stripe: " + order.getTotal());
}
}

// Implementation 2
@Service("paypalProcessor")
public class PayPalPaymentProcessor implements PaymentProcessor {

@Override
public void process(Order order) {
System.out.println("Payment via PayPal: " + order.getTotal());
}
}

// Usage - Option 1: Use @Primary
@Service
public class OrderService {

private final PaymentProcessor paymentProcessor;

// Spring injects StripePaymentProcessor (@Primary)
public OrderService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
}

// Usage - Option 2: Specify with @Qualifier
@Service
public class SpecialOrderService {

private final PaymentProcessor paymentProcessor;

// Force the use of PayPal
public SpecialOrderService(
@Qualifier("paypalProcessor") PaymentProcessor paymentProcessor
) {
this.paymentProcessor = paymentProcessor;
}
}

Chapter Summary​

The 3 Types of Injection​

// 1. Constructor ✅ RECOMMENDED
public MyService(Dependency dep) {
this.dep = dep;
}

// 2. Setter ⚠️ For optional dependencies
@Autowired(required = false)
public void setDependency(Dependency dep) {
this.dep = dep;
}

// 3. Field ❌ TO AVOID
@Autowired
private Dependency dep;

Implementation Choice​

// Option 1: @Primary
@Primary
@Service
public class DefaultImpl implements MyInterface { }

// Option 2: @Qualifier
public MyService(@Qualifier("specificImpl") MyInterface service) { }

Key Points​

✅ Always use constructor injection
✅ Declare dependencies as final
✅ @Autowired optional on constructor
✅ @Primary for default implementation
✅ @Qualifier for specific choice
⚠️ Setter only for optional dependencies
❌ Avoid field injection


Self-Evaluation​

  1. What are the 3 types of injection?
  2. Why is constructor injection preferred?
  3. What is the difference between @Primary and @Qualifier?
  4. How to make a dependency optional?

Next Step​

You now know HOW to inject dependencies. Let's move on to creating and managing Beans!

➡️ Chapter 3: Beans and Spring Context

⬅️ Previous Chapter: IoC

⬅️ Back to Module 5