Inversion of Control(IOC) and Dependency Injection(DI) are core principles of the Spring framework that help to maintain loosely coupled code, are easier to test, and provide great support for high maintainability.
Before diving deep into this, please visit our how to create a clean Spring Framework project tutorial. So that we can proceed to this tutorial.
Let's look into the high-level architectural diagram from Spring documentation.

It shows how the spring container works. Let's look into this.
- Your Business Objects (POJOs): These represent the Java Objects for our application logic.
- Configuration Metadata: This will instruct the container how to instantiate and configure via XML, Java Annotations, or Java Class
- Finally, the Spring Container combines these to produce a fully configured system that is ready to use.
Here, we are going to learn how this process works by building the Notification System. We will look the tight coupling way and how IOC and DI are used to make it loosely coupled architecture.
Understanding the Core Concepts
Inversion of Control(IOC)
IoC is the design principle where the control of object creation and the lifecycle management is transferred to the framework or the container. We don't call the framework; the framework calls us. This is also referred to as Don't call us, we will call you principle.
Traditionally, we use the
new keyword to create an object and manage dependencies ourselves. In the IoC approach, we define what objects need i.e Beans and the IoC handles creating and configuring.Dependency Injection(DI)
DI is the pattern used to implement the IoC principle. It is the process when the objects receive it's dependencies(i.e other objects it requires) from an external source rather than creating them itself.
Tight Coupling (Without IoC and DI)
Let's look into the example; create business object POJOs
java
// Our business object Pojo
public class EmailService {
public void sendNotification(String message, String recipient) {
System.out.println("Email sent to " + recipient + " with message: " + message);
}
}This is the simple EmailService class that will send a notification.
Now, let's create a
NotificationService class that depends on this EmailServicejava
package org.csbyte.notification;
// Our main business object Pojo
public class NotificationService {
// Tight Coupling: The NotificationManager explicitly controls the creation of EmailService
private final EmailService emailService;
public NotificationService() {
this.emailService = new EmailService();
}
public void sendAlert(String message, String userEmail) {
emailService.sendNotification(message, userEmail);
}
}
Why is this a problem?
- If we decided to change from email service to sms we need to completely rewrite the
NotificationServiceclass - We can't do a unit test for NotificationService independently. It needs the instantiation of the email service, and if the email service fails, the test will fail.
Manual Dependency Injection
We can improve this by introducing an interface and shifting the instantiation out of our core service
java
package org.csbyte.notification;
public interface MessageSender {
void sendNotification(String recipient, String message);
}
This is the simple interface for abstraction.
java
package org.csbyte.notification;
public class EmailService implements MessageSender {
@Override
public void sendNotification(String recipient, String message) {
System.out.println("Email Sent to " + recipient + ": " + message);
}
}
Service class for email that implements the interface for sending email.
java
package org.csbyte.notification;
// Our main business object Pojo
public class NotificationService {
// Inject the abstraction via the constructor
private final MessageSender messageSender;
// The dependency is injected, not created here
public NotificationService(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void sendAlert(String userEmail, String message) {
messageSender.sendNotification(userEmail, message);
}
}Here, the
NotificationService is completely decoupled. It doesn't know whether it's sending email ro sms.However, with this approach, someone needs to manually wire these components to work.
java
package org.csbyte.notification;
public class MainApplication {
public static void main(String[] args) {
// Manual wiring boilerplate
MessageSender emailSender = new EmailService();
NotificationService service = new NotificationService(emailSender);
service.sendAlert("alice@gmail.com", "Welcome to the platform!");
}
}As our application grows, this manual wiring code becomes unmaintainable for object creation and configuration. This brings the Spring Framework IoC and DI mechanism.
Applying IoC & DI via Spring
To apply the workflow illustrated in our diagram, we need to transition our system to use the Spring Container. We will provide Spring with our Business Objects (POJOs) and Configuration Metadata, and let it produce a fully configured system.
Let's apply the mechanism that is illustrated in the Spring IOC diagram. We will provide the Business Objects(POJOs) and Configuration Metadata to Spring so that it can produce a fully configured system.
Define an Interface (For Abstraction)
java
package org.csbyte.notification;
public interface MessageSender {
void sendNotification(String recipient, String message);
}Create our Business Objects (POJOs)
Let's implement the interface for email and sms.
java
package org.csbyte.notification;
import org.springframework.stereotype.Component;
@Component("emailService")
public class EmailService implements MessageSender {
@Override
public void sendNotification(String recipient, String message) {
System.out.println("Email Sent to " + recipient + ": " + message);
}
}java
package org.csbyte.notification;
import org.springframework.stereotype.Component;
@Component("smsService")
public class SMSService implements MessageSender{
@Override
public void sendNotification(String recipient, String message) {
System.out.println("SMS Sent to " + recipient + ": " + message);
}
}Now, create the
NotificationService java
package org.csbyte.notification;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
@Service
public class NotificationService {
private final MessageSender messageSender;
// Dependency Injection via Constructor Injection
@Autowired
public NotificationService(@Qualifier("emailService") MessageSender messageSender) {
this.messageSender = messageSender;
}
public void sendAlert(String recipient, String message) {
messageSender.sendNotification(recipient, message);
}
}Here, there is no instantiation. It simply declares a dependency via a constructor.
Note: We are using the default
emailService as the message sender to switch dynamically between email or sms we can simply use the mechanism of utilizing a application.properties file or profiling.Providing Configuration Metadata
Spring needs Configuration Metadata to know where to find these POJOs and how to wire them up.
java
package org.csbyte.notification;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "org.csbyte.notification") // Scans for @Component annotations
public class NotificationAppConfig {
// This metadata tells Spring where to look for our POJOs
}Bringing it Together
Let's look into our main class, where we are going to wrap up everything.
java
package org.csbyte.notification;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class MainApplication {
public static void main(String[] args) {
// We feed Configuration Metadata (NotificationAppConfig.class) and
// Our Business Objects (via Component Scanning) into the Spring Container (ApplicationContext)
ApplicationContext container = new AnnotationConfigApplicationContext(NotificationAppConfig.class);
// The Spring Container manages the lifecycle, injects EmailService into NotificationService,
// and produces a fully configured system Ready for use.
NotificationService manager = container.getBean(NotificationService.class);
// Use the system without worrying about object creation
manager.sendAlert("Your package has shipped!", "john.doe@example.com");
}
}Let's summarize the workflow of the diagram.
- Your Business Objects (POJOs):
EmailService,SMSService, andNotificationService - Configuration Metadata:
@Configuration,@ComponentScan,@Component, and@Autowired - The Spring Container:
AnnotationConfigApplicationContext - Fully configured system, ready for use: The instantiated, wired up
NotificationServicebean retrieved viacontainer.getBean()
We learned how to transfer the tightly coupled application to manual decoupling and later used with spring to understand IoC and DI. IoC and DI shift the complexity of object creation by moving it and managing it by the Spring Container.
