Dependency Injection in Java: A Complete Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: What is Dependency Injection and Why You Need It in Java

In modern Java development, code cleanliness and testability are among the key requirements for a professional. Dependency Injection (DI) is a design pattern that allows you to make your code loosely coupled, flexible, and easily maintainable. Instead of an object creating its own dependencies (for example, via new), DI "injects" them from the outside. This is a fundamental part of the Inversion of Control (IoC) principle.

In this article, we will break down how Dependency Injection works in Java, look at three main ways to implement it (via constructor, setters, and interface), and show how DI is used in real projects, including the popular Spring framework.

Why Does Your Code Become Fragile Without DI?

Imagine a Car class that creates an engine inside itself:

public class Car {
    private Engine engine;

    public Car() {
        this.engine = new PetrolEngine(); // Tight coupling
    }

    public void start() {
        engine.run();
    }
}

The problems with this approach are obvious:

  • Impossible to reuse: You won't be able to easily replace PetrolEngine with ElectricEngine.
  • Difficult to test: For unit tests, you'll have to create a real engine instead of a mock.
  • SRP violation: The Car class is not only responsible for managing the car but also for creating the engine.

Dependency Injection solves these problems by delegating the creation of dependencies to external code (a container or another class).

Three Ways to Inject Dependencies in Java

In Java, there are three main approaches to implementing DI. Let's look at each one using a notification system as an example.

1. Constructor Injection

This is the most preferred method. Dependencies are passed through constructor parameters. This guarantees that the object will always be created in a valid state and that dependencies cannot be changed after creation (immutability).

public interface MessageService {
    void sendMessage(String message, String recipient);
}

public class EmailService implements MessageService {
    @Override
    public void sendMessage(String message, String recipient) {
        System.out.println("Email sent to " + recipient + ": " + message);
    }
}

public class NotificationSender {
    private final MessageService messageService;

    // Dependency injection via constructor
    public NotificationSender(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notifyUser(String userEmail, String msg) {
        messageService.sendMessage(msg, userEmail);
    }
}

// Usage
public class Main {
    public static void main(String[] args) {
        MessageService emailService = new EmailService();
        NotificationSender sender = new NotificationSender(emailService); // DI
        sender.notifyUser("user@example.com", "Hello!");
    }
}

2. Setter Injection

This is suitable for optional dependencies or when a dependency can be changed during the object's lifetime. However, the object can be created in an incomplete state if you forget to call the setter.

public class NotificationSender {
    private MessageService messageService;

    // Setter for dependency injection
    public void setMessageService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void notifyUser(String userEmail, String msg) {
        if (messageService != null) {
            messageService.sendMessage(msg, userEmail);
        } else {
            System.o                

Blogs

Book Recommendations