Manual dependency injection in Java — why and how to do it
Dependency injection (DI) is a widely used design pattern in Java, with a DI container from a framework performing the injection, and through reflection and annotations.
However, it’s possible to do DI without a DI container/framework as well, with objects being instantiated and assembled “manually”. This approach brings great gain in simplicity. It has been given names like manual DI, vanilla DI, and pure DI.
In this article, we’ll understand manual dependency injection and its advantages; how it can be done in Java applications; frameworks embracing it; and the disadvantages.
Quick recap on dependency injection
Dependency injection is a design pattern where objects receive the other objects they depend on instead of creating them. Usually a container from a framework will create and configure the instances and inject them.
DI aims to separate configuration from use, make code easier to maintain and understand, facilitate testing, etc.
Doing it manually
Manual dependency injection is done without a DI container, with the application source code assembling the objects. The assembly of objects is often done in application entry point or factory class.
A class can declare its dependencies in the constructor as usual.
See an example in Jooby, with a controller taking its dependency in the same way:
public class ProductController {
private final ProductRepository repository;
public ProductController(ProductRepository repository) {
this.repository = repository;
}
public void addRoutes(Jooby app) {
app.get("/products", this::list);
}
private String list(Context ctx) {
ctx.setResponseType(MediaType.JSON);
return repository.findNewArrivals();
}
}
And in the main method or in a factory class you can instantiate, configure, and assemble objects:
public static void main(String[] args) {
var sqlContext = SqlContextFactory.create();
var productRepository = new ProductRepository(sqlContext);
var productController = new ProductController(productRepository);
Jooby.runApp(args, app -> {
productController.addRoutes(app);
});
}
The advantages of manual DI
If you are looking for more simplicity in software development, if you consider that currently there is too much complexity, then manual DI may be for you.
Also, manual DI goes very well with an application architecture based more on functional programming and fluent APIs, if you like that.
So, from that point of view, manual DI has great advantages.
It is way simpler.
A developer can get a deeper understanding of how the application works simply by reading its source code.
Consequently, it’s also easier for beginner developers, since it’s one less framework to learn. In the same way, it’s easier for developers coming from other languages and platforms to understand an application and perform maintenance.
With manual DI we identify errors faster, the code won’t compile with wrong injections. The IDE will warn components with wrong injections even before compilation, and will indicate components not being used. Navigation through manual DI graph by IDE is easy.
As manual DI is without magic, the developer feels more control over the application. It’s more pleasant.
It means less external dependencies (in the pom.xml). It’s more lightweight.
Some frameworks backed by large companies are supporting manual DI, helping to spread it. We’ll see more about that in the next topic.
Manual DI support in Java
Since the early 2000s, , it was predominant the enforcement of DI done with frameworks in the Java world, along with the other big frameworks and server applications so popular.
In the middle of the next decade, a new kind of frameworks began to emerge in Java world. They were smaller, more streamlined alternatives to the big frameworks. Many of them were called microframeworks.
And they came to attend the new demands of cloud computing, microservices, and the desire for more simplicity and productivity. Thus, manual DI gained more space.
From these microframeworks, is one of the most interesting. It has a fluent router, is compatible with Netty, Jetty, and Undertow, and is manual DI friendly. I wrote an article about it: Introduction to Jooby.
In 2019, Oracle released the first version of , a set of libraries for microservices. It embraces manual DI and is aligned with fluent APIs and functional programming. And it has the highlight of being developed by a large company, the same that develops the Java platform.
More examples of frameworks and libraries more friendly to manual DI could be mentioned like , , …
Hopefully other frameworks will start to welcome manual DI at some point as well. Certainly they would benefit from it, becoming more lightweight.
The disadvantages
DI framework containers dominate the market today, and many Java frameworks and tools are based on it.
Manual DI is not well accepted by Java developers yet, who may not like its adoption in projects.
Some notes
One: do not confuse Service Locator with manual DI because they are two different patterns.
The service locator pattern is when objects get their dependencies by invoking methods of an object that has the instances of the dependencies, which is the service locator. For example:
public class ProductController {
private final ProductRepository repository;
public ProductController() {
this.repository = ServiceLocator.getProductRepository();
}
Two: different terms have been used for DI done without a DI framework, such as vanilla DI and . But manual DI is the most traditional in Java.
Conclusion
Manual DI brings simplicity to Java applications and works even better with an architecture that takes advantage of fluent APIs and functional programming.
Now I want to know: would you use manual dependency injection in a future project? Respond in the comments section.