Introduction: The Problem of Tight Coupling in Java
\n\nImagine you are writing an application for sending notifications. In your NotificationService class, an EmailSender object is created directly. Everything works perfectly until one day the client asks you to add support for SMS and Push notifications. You will have to rewrite NotificationService, change constructors, and add new conditions. The code becomes fragile, difficult to test and extend.
This is the classic problem of tight coupling. The solution is called Dependency Injection — one of the key design principles in Java and the foundation of frameworks like Spring. In this article, we will break down what Dependency Injection is in simple terms, how to implement it, and why no professional Java developer can do without it.
\n\nWhat is Dependency Injection in Simple Terms?
\n\nDependency Injection (DI) is a design pattern in which an object receives its dependencies from the outside, rather than creating them itself. Instead of writing new EmailSender() inside a class, you "inject" a ready-made object through a constructor, setter, or interface.
DI is closely related to the principle of Inversion of Control (IoC). If previously your class itself managed the creation of dependencies, now control is transferred to an external container (e.g., Spring IoC).
\n\nMain advantages of DI:
\n- \n
- Loose coupling — classes depend on abstractions, not concrete implementations. \n
- Simplified testing — dependencies are easily replaced with mocks or stubs. \n
- Code reusability — the same service can be used in different contexts. \n
- Ease of maintenance — changing an implementation does not require modifying consumer classes. \n
Types of Dependency Injection in Java
\n\nThere are three main ways to implement Dependency Injection in Java. Let's look at each with an example.
\n\n1. Constructor Injection
\n\nThe most common and recommended method. Dependencies are passed through constructor parameters and stored as class fields (usually final).
// Interface for sending messages\npublic interface MessageSender {\n void send(String message, String recipient);\n}\n\n// Concrete implementation — Email\npublic class EmailSender implements MessageSender {\n @Override\n public void send(String message, String recipient) {\n System.out.println("Sending Email: " + message + " to " + recipient);\n }\n}\n\n// Service that uses DI via constructor\npublic class NotificationService {\n private final MessageSender sender;\n\n // Dependency is injected through the constructor\n public NotificationService(MessageSender sender) {\n this.sender = sender;\n }\n\n public void notifyUser(String message, String userEmail) {\n sender.send(message, userEmail);\n }\n}\n\n// Usage\npublic class Main {\n public static void main(String[] args) {\n MessageSender emailSender = new EmailSender();\n NotificationService service = new NotificationService(emailSender);\n service.notifyUser("Welcome!", "user@example.com");\n }\n}\n\nPros: the object is created in a fully ready state, dependencies are immutable, easy to test.
\n\n2. Setter Injection
\n\nDependencies are passed through setter methods after the object is created. It is more often used for optional dependencies.
\n\npublic class NotificationService {\n private MessageSender sender;\n\n // Setter for dependency injection\n public void setSende