Skip to main content

TP5.1 - Advanced Dependency Injection

Objectives​

By the end of this lab, you will be able to:

  1. ✅ Use the 3 types of injection (constructor, setter, field)
  2. ✅ Choose between multiple implementations with @Qualifier
  3. ✅ Manage optional dependencies
  4. ✅ Understand and resolve circular dependencies

Estimated duration: 1h30


Prerequisites​

  • ✅ Chapters 1 and 2 of Module 5 completed
  • ✅ Maven 3.9+ installed
  • ✅ Java 17+ installed
  • ✅ VS Code with Java extensions

Preparation: Create the Project​

Step 1: Generate the Project with Spring Initializr​

Via the website:

  1. Go to https://start.spring.io/

  2. Configure:

    • Project: Maven
    • Language: Java
    • Spring Boot: 3.2.0 (or latest version)
    • Group: com.example
    • Artifact: injection-demo
    • Name: injection-demo
    • Package name: com.example.injection
    • Packaging: Jar
    • Java: 17
  3. Dependencies to add:

    • Spring Web
  4. Click on GENERATE

  5. Unzip and open in VS Code


Step 2: Project Structure​

Create the following structure:

src/main/java/com/example/injection/
├── InjectionDemoApplication.java
├── service/
│ ├── EmailService.java
│ ├── GmailService.java
│ ├── SendGridService.java
│ └── NotificationService.java
├── controller/
│ └── TestController.java
└── config/
└── AppConfig.java

Exercise 1: Constructor Injection ✅​

Objective​

Create a service with constructor injection (best practice).

Step 1.1: Create the EmailService Interface​

File: service/EmailService.java

package com.example.injection.service;

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

What we're doing: Create an interface to have multiple possible implementations.


Step 1.2: Create the GmailService Implementation​

File: service/GmailService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class GmailService implements EmailService {

@Override
public void send(String to, String message) {
System.out.println("[Gmail] Sending email to " + to + ": " + message);
}

@Override
public String getProviderName() {
return "Gmail";
}
}

Key points:

  • @Service → Spring manages this Bean
  • Implements EmailService

Step 1.3: Create the NotificationService with Constructor Injection​

File: service/NotificationService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class NotificationService {

// Dependency declared as 'final' (immutable)
private final EmailService emailService;

// Constructor injection (BEST PRACTICE ✅)
public NotificationService(EmailService emailService) {
this.emailService = emailService;
System.out.println("NotificationService created with: " +
emailService.getProviderName());
}

public void notifyUser(String email, String message) {
System.out.println("=== NotificationService ===");
emailService.send(email, message);
}
}

Key points:

  • private final → Immutable
  • Injection via constructor
  • No need for @Autowired (optional on single constructor)

Step 1.4: Create the Test Controller​

File: controller/TestController.java

package com.example.injection.controller;

import com.example.injection.service.NotificationService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/test")
public class TestController {

private final NotificationService notificationService;

// Constructor injection also here
public TestController(NotificationService notificationService) {
this.notificationService = notificationService;
}

@GetMapping("/constructor")
public String testConstructorInjection() {
notificationService.notifyUser("user@example.com",
"Test constructor injection");
return "✅ Constructor injection tested - See console";
}
}

Step 1.5: Test​

Start the application:

mvn clean install -DskipTests spring-boot:run

Test in the browser:

http://localhost:8080/api/test/constructor

Console should display:

NotificationService created with: Gmail
=== NotificationService ===
[Gmail] Sending email to user@example.com: Test constructor injection

✅ Expected result: "✅ Constructor injection tested - See console"


Exercise 2: Manage Multiple Implementations with @Qualifier​

Objective​

Have 2 implementations of EmailService and choose which one to use.

Step 2.1: Create SendGridService​

File: service/SendGridService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service("sendGridService") // ← Explicit name
public class SendGridService implements EmailService {

@Override
public void send(String to, String message) {
System.out.println("[SendGrid] Sending email to " + to + ": " + message);
}

@Override
public String getProviderName() {
return "SendGrid";
}
}

Step 2.2: Problem - Two Implementations!​

Restart the application:

mvn clean install -DskipTests spring-boot:run

You should see an ERROR:

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.example.injection.service.NotificationService
required a single bean, but 2 were found:
- gmailService: defined in file [...]
- sendGridService: defined in file [...]

Action:

Consider marking one of the beans as @Primary, or use @Qualifier

This is normal! Spring doesn't know which one to choose.


Step 2.3: Solution 1 - @Primary​

Modify GmailService.java:

package com.example.injection.service;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

@Service
@Primary // ← Default implementation
public class GmailService implements EmailService {

@Override
public void send(String to, String message) {
System.out.println("[Gmail] Sending email to " + to + ": " + message);
}

@Override
public String getProviderName() {
return "Gmail";
}
}

Restart: The application now starts! Gmail is used by default.

Test:

http://localhost:8080/api/test/constructor

Console:

[Gmail] Sending email to user@example.com: ...

Step 2.4: Solution 2 - @Qualifier to Choose​

Create a new service that uses SendGrid:

File: service/SendGridNotificationService.java

package com.example.injection.service;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;

@Service
public class SendGridNotificationService {

private final EmailService emailService;

// Use @Qualifier to choose SendGrid
public SendGridNotificationService(
@Qualifier("sendGridService") EmailService emailService
) {
this.emailService = emailService;
System.out.println("SendGridNotificationService created with: " +
emailService.getProviderName());
}

public void notifyUser(String email, String message) {
System.out.println("=== SendGridNotificationService ===");
emailService.send(email, message);
}
}

Step 2.5: Test @Qualifier​

Add a method in TestController.java:

@RestController
@RequestMapping("/api/test")
public class TestController {

private final NotificationService notificationService;
private final SendGridNotificationService sendGridNotificationService;

public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
}

@GetMapping("/constructor")
public String testConstructorInjection() {
notificationService.notifyUser("user@example.com",
"Test with Gmail");
return "✅ Gmail used - See console";
}

@GetMapping("/qualifier")
public String testQualifier() {
sendGridNotificationService.notifyUser("user@example.com",
"Test with SendGrid");
return "✅ SendGrid used via @Qualifier - See console";
}
}

Test:

http://localhost:8080/api/test/qualifier

Console:

=== SendGridNotificationService ===
[SendGrid] Sending email to user@example.com: Test with SendGrid

✅ You have 2 services using different implementations!


Exercise 3: Optional Dependencies​

Objective​

Create a service that can work with or without a dependency.

Step 3.1: Create an SmsService (Optional)​

File: service/SmsService.java

package com.example.injection.service;

public interface SmsService {
void send(String phone, String message);
}

Do NOT create an implementation yet!


Step 3.2: Create CompleteNotificationService with Optional Dependency​

File: service/CompleteNotificationService.java

package com.example.injection.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class CompleteNotificationService {

private final EmailService emailService;
private final Optional<SmsService> smsService; // ← Optional!

public CompleteNotificationService(
EmailService emailService,
Optional<SmsService> smsService // ← Optional Wrapper
) {
this.emailService = emailService;
this.smsService = smsService;

System.out.println("CompleteNotificationService created");
System.out.println("- Email: " + emailService.getProviderName());
System.out.println("- SMS: " + (smsService.isPresent() ? "Available" : "Not available"));
}

public void notify(String email, String phone, String message) {
// Email always sent
emailService.send(email, message);

// SMS only if available
smsService.ifPresent(sms -> sms.send(phone, message));
}
}

Key points:

  • Optional<SmsService> → May be absent
  • ifPresent() → Executed only if present

Step 3.3: Test Without SmsService​

Add in TestController.java:

private final CompleteNotificationService completeService;

public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService,
CompleteNotificationService completeService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
this.completeService = completeService;
}

@GetMapping("/optional")
public String testOptional() {
completeService.notify("user@example.com", "123-456-7890",
"Test optional dependency");
return "✅ Email sent, SMS not available - See console";
}

Test:

http://localhost:8080/api/test/optional

Console:

CompleteNotificationService created
- Email: Gmail
- SMS: Not available
[Gmail] Sending email to user@example.com: Test optional dependency

✅ The application works even without SmsService!


Step 3.4: Add SmsService and Retest​

File: service/SimpleSmsService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

@Service
public class SimpleSmsService implements SmsService {

@Override
public void send(String phone, String message) {
System.out.println("[SMS] Sending to " + phone + ": " + message);
}
}

Restart and test:

http://localhost:8080/api/test/optional

Console:

CompleteNotificationService created
- Email: Gmail
- SMS: Available
[Gmail] Sending email to user@example.com: Test optional dependency
[SMS] Sending to 123-456-7890: Test optional dependency

✅ Now both are used!


Exercise 4: Inject All Implementations​

Objective​

Retrieve ALL implementations of an interface.

Step 4.1: Create MultiChannelNotificationService​

File: service/MultiChannelNotificationService.java

package com.example.injection.service;

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MultiChannelNotificationService {

private final List<EmailService> allEmailServices;

// Spring injects a List with ALL implementations
public MultiChannelNotificationService(List<EmailService> allEmailServices) {
this.allEmailServices = allEmailServices;
System.out.println("MultiChannelNotificationService created with " +
allEmailServices.size() + " email services:");
allEmailServices.forEach(service ->
System.out.println(" - " + service.getProviderName())
);
}

public void broadcastEmail(String to, String message) {
System.out.println("=== Sending via ALL providers ===");
allEmailServices.forEach(service -> service.send(to, message));
}
}

Step 4.2: Test​

Add in TestController.java:

private final MultiChannelNotificationService multiChannelService;

public TestController(
NotificationService notificationService,
SendGridNotificationService sendGridNotificationService,
CompleteNotificationService completeService,
MultiChannelNotificationService multiChannelService
) {
this.notificationService = notificationService;
this.sendGridNotificationService = sendGridNotificationService;
this.completeService = completeService;
this.multiChannelService = multiChannelService;
}

@GetMapping("/all")
public String testAllImplem() {
multiChannelService.broadcastEmail("user@example.com",
"Broadcast message");
return "✅ Sent via all providers - See console";
}

Test:

http://localhost:8080/api/test/all

Console:

=== Sending via ALL providers ===
[Gmail] Sending email to user@example.com: Broadcast message
[SendGrid] Sending email to user@example.com: Broadcast message

✅ Both implementations are used!


Exercise 5: Setter Injection (Optional Dependencies)​

Objective​

Use setter injection for truly optional dependencies.

Step 5.1: Create LoggerService with Setter​

File: service/LoggerService.java

package com.example.injection.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class LoggerService {

private SmsService smsService; // ← Not 'final', can be null

// Setter injection - OPTIONAL
@Autowired(required = false) // ← required = false
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
System.out.println("SMS Service injected into LoggerService");
}

public void logNotification(String message) {
System.out.println("[LOG] " + message);

if (smsService != null) {
System.out.println("[LOG] SMS Service available");
} else {
System.out.println("[LOG] SMS Service not available");
}
}
}

Key points:

  • @Autowired(required = false) → Not mandatory
  • Check if (smsService != null)

Step 5.2: Test​

Add in TestController.java:

private final LoggerService loggerService;

@GetMapping("/setter")
public String testSetter() {
loggerService.logNotification("Test setter injection");
return "✅ Setter injection tested - See console";
}

Test:

http://localhost:8080/api/test/setter

Console (with SmsService):

SMS Service injected into LoggerService
[LOG] Test setter injection
[LOG] SMS Service available

Recap: Comparison of Methods​

Comparison Table​

TypeSyntaxMandatory?Immutable?Usage
Constructorpublic Service(Dep d)✅ Yes✅ Yes (final)Recommended for mandatory dependencies
Setter@Autowired(required=false)❌ No❌ NoOptional dependencies
Field@Autowired private Dep d⚠️ Yes❌ No❌ To avoid
Optional<T>Optional<Dep> d❌ No✅ YesModern optional dependencies
List<T>List<Dep> all✅ Yes✅ YesAll implementations

Complete Final Code​

Your project should now have:

✅ GmailService (@Primary)
✅ SendGridService
✅ NotificationService (constructor + Gmail)
✅ SendGridNotificationService (constructor + @Qualifier)
✅ CompleteNotificationService (Optional<SmsService>)
✅ SimpleSmsService (optional)
✅ MultiChannelNotificationService (List<EmailService>)
✅ LoggerService (setter + required=false)

Self-Assessment​

Answer the following questions:

  1. Why is constructor injection preferred?
  2. What is the difference between @Primary and @Qualifier?
  3. How to manage a truly optional dependency?
  4. How to inject all implementations of an interface?
  5. When to use Optional<T> vs @Autowired(required=false)?

If you can answer all these questions, you have mastered dependency injection! ✅


Bonus Challenge 🎯​

Create a multi-channel notification system:

  1. Interface NotificationChannel
  2. Implementations: EmailChannel, SmsChannel, PushChannel
  3. Service NotificationOrchestrator that:
    • Injects all channels
    • Allows enabling/disabling channels
    • Sends on all active channels

Estimated time: 30 minutes


Next Step​

You now master dependency injection! Move on to TP2 to learn about Beans and Scopes.

➡️ TP5.2: Beans and Scopes

⬅️ Back to Module 5