Dependency Injection

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 @Autowired is optional.
  • Setter injection: dependencies arrive through a setter method. It suits optional or re-configurable dependencies.
  • Field injection: @Autowired directly 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

Scroll to Top