Sitemap
Swaminathan Rajagopalan

Swaminathan Rajagopalan

Software enthusiast passionate about clean code - Java, and Python. I simplify complex concepts and make powerful ideas easy to grasp. Learnovate Academy

Understanding Dependency Injection with Java

5 min readSep 16, 2024

--

If you are a Java backend programmer, you would have definitely heard about this term, Dependency Injection (DI). This is one of the fundamental concepts in software engineering and a much-followed design pattern to manage objects and loose coupling that helps you to develop scalable, modular, maintainable and testable applications. In this article we will dive in and learn about this concept more in detail.

Why this technique is important for a Java developer?

Loose coupling is a design principle that aims to reduce the interdependence between components of a system. When objects are loosely coupled, changes done to one object do not require changes to other objects. The primary advantage of dependency Injection is it enables loose coupling between objects.

DI works on the principle that the responsibility of creating an object within the class or a function is separated from that class or function. Instead, we use the interfaces that contracts the methods and properties that class has to implement. So, the class that needs the object depends only on the interface and not on the concrete implementation.

Let’s Understand

The concept of loose coupling may look confusing and so I will try to make it simple with the help of an illustration Suppose we need to write an application that sends SMS to the given mobile number.

Let’s say we have an application where we consume SMSService to send SMS. Normally we would implement the class as below.

public class SMSService {
public void sendSMS(String message, String mobileNum){
System.out.println("SMS sent to "+ mobileNum + " Message="+message);
}
}

This is the master class and the send SMS method takes 2 parameters and will have the logic to send the message to the given mobile number.

Our application code will be like below.

public class SMSApplication {
private SMSService sms_service = new SMSService ();

public void processMessage(String msg, String mobNumber){
this. sms_service.sendSMS (msg, mobNumber);
}
}

Our client code that will use SMSApplication class to send SMS messages will be like below.

public class Main {
public static void main(String[] args) {
SMSApplication1 app = new SMSApplication1();
app.processMessage("Hello Rahul", "123456789");
}
}

At first look, the code looks good and satisfies our requirement. But if you look into it deeper, you will see lot of limitations in this code pattern.

  1. SMSApplication class is responsible to initialize the SMS service. This creates a hard-coded dependency. If we want to move over to some other SMS service in the future, it will require code changes in SMSApplication class. It becomes further complicated if we use this SMS service in multiple classes and then we need to change the implementation everywhere. This makes our application hard to extend.
  2. In future, if you want to add another service, say sending emails along with the SMS, then we would need to write another application for that. This will again involve code changes in Main class and all other class where we have already used the SMSApplication class.
  3. As we are directly creating the SMS Application instance inside the Main class, creating test cases and mocking these tests are not possible

So, what shall we do?

One way to improve this code is to remove the SMS service instance creation from SMSApplication class by having a constructor that requires SMS service as an argument.

public class SMSApplication {
SMSService smsService = null;
public SMSApplication(SMSService service){
this.smsService = service;
}

public void processMessages(String msg, String mobNumber){
this. smsService. sendSMS (msg, mobNumber);
}
}

Then our Main.java class will look like this

public class Main {
public static void main(String[] args) {
SMSService sms_service = new SMSService ();
SMSApplication app = new SMSApplication(sms_service);
app.processMessages("Hello Rahul", "123456789");
}
}

In this case, we are asking client application to initializing the SMS service and that is not a good design decision.

In comes DI

This is where our dependency Injection pitches in. Let’s see how java dependency injection pattern helps to solve the problems with the above implementation.

Get Swaminathan Rajagopalan’s stories in your inbox

Join Medium for free to get updates from this writer.

To implement the dependency injection, we need the following requirements.

  1. Service components should be designed with base class or interface. Interfaces are preferred over classes as they provide contract for the services that can be implemented.
  2. Consumer classes should be written based on these interfaces.
  3. Need to have Injector classes that will initialize the services and then the consumer classes.

So first, we need to create an interface.Let us create an interface named MessageService

public interface MessageService {
void sendMessage(String msg, String receiver);
}

This provides a contract sendMessage. Next, we will create an implementation class, that will implement this interface

public class SMSServiceImpl implements MessageService{
@Override
public void sendMessage(String msg, String receiver) {
System.out.println("SMS : "+ msg + " sent to : "+ receiver);
}
}

Now the service implementation will be like this

public class SMSApplication implements Client{
private MessageService service;

public SMSApplication (MessageService msgSrc){
this.service= msgSrc;
}

public void processMessages(String msg, String receiver){
this.service.sendMessage(msg, receiver);
}
}

Notice that our application class is just using the service. It does not initialize the service that leads to better “separation of concerns”.

Creating Injectors

We are not finished yet. We still create object in the client side (object for SMSServiceImpl.java). if we can remove that dependency also it becomes a perfect solution. We will do this by creating an injector class.

Let us create an interface named Client

public interface Client {
void processMessages(String msg, String rec);
}

Let’s have an interface MessageInjector with a method declaration that returns the Client class.

public interface MessageInjector {
public Client getClient();
}

Now for the SMS service, we will have to create injector class as below. Later, if we extend our services by sending emails also, we need to create an injector implementation for that.

public class SMSServiceInjector implements MessageInjector {
@Override
public Client getClient() {
return new SMSApplication(new SMSServiceImpl());
}
}

Finally let’s see how our client applications will use the application with a simple program.

public class Main {

public static void main(String[] args) {
String msg = "Hi Rahul";
String mobile = "123456789";

//send SMS
MessageInjector injector = new SMSServiceInjector();
Client client = injector.getClient();
client.processMessages(msg,mobile);
}
}

As you see here, we don’t create any object of the class that sends the SMS.

When we work with Spring Framework, we say it is using dependency injection. What it actually does is, it creates the objects and keeps it in the memory and injects them into our classes as and when required. We use the auto wired annotation and inject the object we require into our classes.

Advantages of Dependency Injection

There are many advantages we can talk about DI. A few of them here.

  1. We achieve Separation of Concerns
  2. As objects are initialized by the injectors, lots of boiler plate coding is avoided
  3. Applications becomes easily extendable. You can easily add an email service in our above example, without modifying the existing client code.
  4. Mock testing becomes simple and easier.

Disadvantages

There are some downsides also

  1. If not properly used, we may face issues as the effect of changes are known only at runtime.
  2. Dependency injection in java hides the service class dependencies that can lead to runtime errors that would have been caught at compile time.

Conclusion

Dependency Injection helps you to achieve loose coupling that separates the responsibility of creating objects in classes that require them. we can inject the dependencies from external sources that reduces lot of boiler plate coding in your application. Dependency injection can be achieved through constructor injection, setter injection, or field injection, depending on the framework being used.

If this pattern is properly used, this gives you a whole lot of goodness to your application. Hope this article provided some useful information on dependency injection and you learnt something new today.

--

--

Swaminathan Rajagopalan
Swaminathan Rajagopalan

Written by Swaminathan Rajagopalan

Software enthusiast passionate about clean code - Java, and Python. I simplify complex concepts and make powerful ideas easy to grasp. Learnovate Academy