Dependency Injection (DI) means a class does not create the objects it needs; Spring creates them and hands them in. Spring looks at the constructor, setter, or field, finds a matching bean in the container, and injects it automatically. It is used for loose coupling, easy swapping of implementations (for example Email vs SMS), and simple unit testing with mocks. It also keeps dependencies explicit, so a class cannot be built in a half-configured state.
Types
- Constructor injection (recommended): dependencies arrive through the constructor. They can be
final, are never null, and with a single constructor@Autowiredis optional. - Setter injection: dependencies arrive through a setter method. It suits optional or re-configurable dependencies.
- Field injection:
@Autowireddirectly on a field. It is short, but discouraged because it hides dependencies and is hard to test. - Choosing between several beans:
@Primary(default choice),@Qualifier("beanName")(explicit choice).
Hands-on Experiment: Creating a spring boot application along with dependency injection
The following steps are involved in developing this hands-on experiment.
Step 1: Generate the Project
Step 2: Project Structure
Step 3: pom.xml
Step 4: application.properties
Step 5: Create Entity Class
Step 6: Create Repository Interface
Step 7: Create Service Class
Step 8: Create Controller Class
Step 9: Main Application Class
Step 10: Run the Main Application Class
Step 1: Configure the Project on Spring Initializr
Go to start.spring.io and set:
- Project: Maven
- Language: Java
- Spring Boot version: 3.2.x
- Group: com.example
- Artifact: studentdi
- Name: studentdi
- Package name: com.example.studentdi
- Packaging: Jar
- Java: 17
- Dependencies: Spring Web, Spring Data JPA, MySQL Driver
Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.
Step 2: Generated Project Structure

Step 3: Explore pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>student-di-demo</artifactId>
<version>1.0.0</version>
<name>student-di-demo</name>
<description>Dependency Injection demo - Student application</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Step 4: application.yml
spring:
application:
name: student-di-demo
datasource:
url: jdbc:mysql://localhost:3306/student_di_db?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.MySQLDialect
server:
port: 8082
Step 5: Create Entity Class
package com.example.studentdi.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
@Entity
@Table(name = "students")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Invalid email")
@NotBlank(message = "Email is required")
@Column(unique = true)
private String email;
private String course;
public Student() {}
public Student(String name, String email, String course) {
this.name = name;
this.email = email;
this.course = course;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getCourse() { return course; }
public void setCourse(String course) { this.course = course; }
}
Step 6: Create below Repository Interfaces
package com.example.studentdi.repository;
import com.example.studentdi.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
List<Student> findByCourse(String course);
boolean existsByEmail(String email);
}
package com.example.studentdi.service;
// One interface, two implementations -> shows how Spring chooses what to inject
public interface NotificationService {
String notifyStudent(String email, String message);
}
Step 7: Create below Service classes
package com.example.studentdi.service;
import com.example.studentdi.entity.Student;
import com.example.studentdi.repository.StudentRepository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.NoSuchElementException;
@Service
public class StudentService {
private final StudentRepository studentRepository;
private final NotificationService notificationService;
// Constructor injection of TWO dependencies.
// NotificationService has 2 beans; Spring picks the @Primary one (Email).
public StudentService(StudentRepository studentRepository,
NotificationService notificationService) {
this.studentRepository = studentRepository;
this.notificationService = notificationService;
System.out.println(">>> StudentService created with: "
+ notificationService.getClass().getSimpleName());
}
public Student create(Student student) {
if (studentRepository.existsByEmail(student.getEmail())) {
throw new IllegalArgumentException("Email already exists: " + student.getEmail());
}
Student saved = studentRepository.save(student);
notificationService.notifyStudent(saved.getEmail(), "Welcome " + saved.getName() + "!");
return saved;
}
public List<Student> findAll() {
return studentRepository.findAll();
}
public List<Student> findByCourse(String course) {
return studentRepository.findByCourse(course);
}
public Student findById(Long id) {
return studentRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("Student not found with id " + id));
}
public Student update(Long id, Student updated) {
Student existing = findById(id);
existing.setName(updated.getName());
existing.setEmail(updated.getEmail());
existing.setCourse(updated.getCourse());
return studentRepository.save(existing);
}
public void delete(Long id) {
studentRepository.delete(findById(id));
}
}
package com.example.studentdi.service;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
// @Primary -> chosen by default whenever a NotificationService is requested
@Primary
@Service
public class EmailNotificationService implements NotificationService {
@Override
public String notifyStudent(String email, String message) {
String result = "EMAIL to " + email + ": " + message;
System.out.println(">>> " + result);
return result;
}
}
package com.example.studentdi.service;
import org.springframework.stereotype.Service;
// Not primary -> injected only when requested with @Qualifier("smsNotificationService")
@Service
public class SmsNotificationService implements NotificationService {
@Override
public String notifyStudent(String email, String message) {
String result = "SMS for " + email + ": " + message;
System.out.println(">>> " + result);
return result;
}
}
Step 8: Create Controller class
package com.example.studentdi.controller;
import com.example.studentdi.entity.Student;
import com.example.studentdi.service.StudentService;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/students")
public class StudentController {
// final -> the dependency can never be null or changed after construction
private final StudentService studentService;
// THE EXPERIMENT: constructor injection.
// With a single constructor, @Autowired is optional - Spring injects StudentService automatically.
public StudentController(StudentService studentService) {
this.studentService = studentService;
System.out.println(">>> StudentController created, StudentService injected via constructor");
}
@PostMapping
public ResponseEntity<Student> create(@Valid @RequestBody Student student) {
return ResponseEntity.status(HttpStatus.CREATED).body(studentService.create(student));
}
@GetMapping
public List<Student> getAll(@RequestParam(required = false) String course) {
return (course == null) ? studentService.findAll() : studentService.findByCourse(course);
}
@GetMapping("/{id}")
public Student getById(@PathVariable Long id) {
return studentService.findById(id);
}
@PutMapping("/{id}")
public Student update(@PathVariable Long id, @Valid @RequestBody Student student) {
return studentService.update(id, student);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
studentService.delete(id);
return ResponseEntity.noContent().build();
}
}
package com.example.studentdi.controller;
import com.example.studentdi.service.NotificationService;
import com.example.studentdi.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;
import java.util.LinkedHashMap;
import java.util.Map;
// Shows all three injection styles side by side.
@RestController
@RequestMapping("/api/di")
public class InjectionTypesController {
// 1) Constructor injection (recommended)
private final StudentService studentService;
// 2) Setter injection (for optional dependencies)
private NotificationService setterInjected;
// 3) Field injection (shortcut; discouraged - hard to test, hides dependencies)
@Autowired
@Qualifier("smsNotificationService")
private NotificationService fieldInjected;
public InjectionTypesController(StudentService studentService) {
this.studentService = studentService;
}
@Autowired
public void setSetterInjected(NotificationService setterInjected) {
this.setterInjected = setterInjected; // @Primary bean -> Email
}
// GET /api/di/types -> which concrete class Spring injected in each style
@GetMapping("/types")
public Map<String, String> types() {
Map<String, String> result = new LinkedHashMap<>();
result.put("constructorInjection (StudentService)", studentService.getClass().getSimpleName());
result.put("setterInjection (NotificationService, @Primary)", setterInjected.getClass().getSimpleName());
result.put("fieldInjection (NotificationService, @Qualifier sms)", fieldInjected.getClass().getSimpleName());
return result;
}
// POST /api/di/notify -> uses the field-injected SMS bean
@PostMapping("/notify")
public Map<String, String> notify(@RequestBody Map<String, String> body) {
String result = fieldInjected.notifyStudent(body.get("email"), body.get("message"));
return Map.of("result", result);
}
}
package com.example.studentdi.config;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NoSuchElementException.class)
public ResponseEntity<Map<String, String>> notFound(NoSuchElementException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("error", ex.getMessage()));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> badRequest(IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of("error", ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> validation(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(e -> errors.put(e.getField(), e.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
}
Step 9: Main Application
package com.example.studentdi;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class StudentDiApplication {
public static void main(String[] args) {
SpringApplication.run(StudentDiApplication.class, args);
}
}
Step 10: Run the StudentDiApplication


