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: Why Dependency Injection is a must-have for Java developers

Every Java developer sooner or later faces the problem of tight code coupling. When one class directly creates instances of other classes, the system becomes fragile, difficult to test and modify. The solution to this problem is Dependency Injection (DI). This is one of the key principles of modern development, underlying frameworks such as Spring and Jakarta EE.

In this article, we will break down what DI is, how it differs from IoC (Inversion of Control), look at three main ways to inject dependencies in Java with clear code examples, and discuss why this pattern is necessary for creating maintainable applications.



What is Dependency Injection and Inversion of Control?

Before moving on to the code, it is important to understand two fundamental concepts.

Inversion of Control (IoC) is a general principle where the program's control flow is transferred to an external framework or container. Instead of the code itself deciding when to create objects and call methods, an external system handles this.

Dependency Injection is a specific implementation of the IoC principle. It is a pattern where an object receives its dependencies (other objects it depends on) from the outside, rather than creating them itself.

Simply put, instead of the Car class creating the engine itself (new Engine()), it receives a ready-made engine through a constructor or setter. This makes the Car class independent of the specific Engine implementation.



Three ways to inject dependencies in Java

In Java, there are three main ways to implement DI. Let's look at each of them using a simple notification system as an example.



1. Constructor Injection

This is the most recommended and safest method. Dependencies are passed through the class constructor. This guarantees that the object will never be created in an invalid state, since all necessary dependencies are mandatory.

// Interface for the notification servicepublic interface MessageService {    void sendMessage(String message, String recipient);}

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

// Class that uses the notification servicepublic class NotificationSender { private final MessageService service; // Dependency

// Constructor injection public NotificationSender(MessageService service) { this.service = service; }

public void notifyUser(String user, String msg) { this.service.sendMessage(msg, user); }}

// Usagepublic 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 method is used when the dependency is not mandatory or can be changed after the object is created. The dependency is passed through a public setter method.

public class NotificationSenderWithSetter {    private MessageService service; // Dependency is not mandatory

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

public void notifyUser(String user, String msg) { if (service != null) { this.service.sendMessage(msg, user); } else { System.out.println("No service configured!"); } }}

// Usage

Blogs

Book Recommendations