The Value of Dependency Injection with Java Spring
Building a large-scale web application can be daunting, and complexity can add up very quickly. There are tons of frameworks out there that make the process easier by abstracting away boilerplate, but learning to leverage these frameworks can be challenging. Understanding them at a deeper level can seem impossible when there are so many moving parts.
I was recently working on a personal project with a couple of friends, and was introducing them to Spring Boot to write our back end. Spring is the standard for creating Java web servers and is an extremely powerful tool once you know how it works. We had made significant progress on the server-side code when my friend stopped and asked
“Just what exactly is a bean? When do I make one? How do I make one?”
In short, a bean is just an instance of any Java class that Spring instantiates, stores, and can inject across your application. They are integral in dependency injection, one of Spring’s most valuable tools.
Ok… cool. Now what? With so many articles and tutorials that show you how to get up and running with Spring, understanding the why and how of Spring’s toolset can get lost in the mess of technical terms.
To this point, I want to take a minute to discuss the value of one of Spring’s core mechanisms: dependency injection. To be clear, this is not a how-to for creating a functioning application, just a thought experiment about why we use dependency injection and a clarification of some Spring jargon. Now let’s get into it.
Let’s say we’re writing a delivery service app which has two main methods of delivery, air shipping and ground shipping. The AirShipping class uses a Plane to send packages while the GroundShipping class uses a Truck. Both planes and trucks need a Driver, which in turn needs a name. Our business is small at the moment so we only have one plane and one truck. A first attempt at implementing these classes is shown below.
For the sake of this analogy, let’s assume Bob is an incredibly versatile and hardworking deliveryman tasked with all of our company’s deliveries (at least until we raise a Series A, can triple our employee base and get cold brew on tap).
The example written above functions perfectly fine, but has a few flaws:
- The GroundShipping and AirShipping classes are both intimately involved with the configuration of their delivery vehicles. These classes have no reason to care about who’s driving (isn’t that the whole point of encapsulation anyway?). All they should worry about is handing off a ship-ready package to the proper delivery vehicle.
- The driver name is repeated in two places, and there are two identical Driver objects sitting in memory!
- It is very hard to switch out the Driver. We would have to make code changes in multiple places and risk breaking unrelated logic just to change the name!
- Since the shipping classes control the creation of their Truck and Plane dependencies, they must know the concrete classes were we to hide either of the vehicles behind an interface.
But with dependency injection, we can solve all of these problems! First, let’s discuss just what in the world a bean is.
Each class you write in your application is likely to have dependencies. These dependencies have their own dependencies and so on until your entire code base is one large mess of a tree. The user interacts with the app’s entry point (the root of the tree) which causes a chain of interactions between classes down to the data layer and back up. Wiring these classes together can result in fragile and tightly coupled code like the examples above. Dependency injection is the handling of this wiring for you.
To do this, we tell Spring which classes we want it to wire in across the application (like Plane and Truck). On application start up, it creates instances of these classes that it stores in the “Spring Context”. These instances are called beans. When Spring comes across another class that has a dependency (e.g. a Plane or a Truck), it reaches inside its context for a bean of the matching type and plugs it in for that field. By making beans of all your singleton classes, Spring can wire together your entire application.
Note: Bean creation can happen through many different ways like the method annotation @Bean or through class annotations such as @Component or @Service . All these annotations have their own use cases but I have used the @Bean method since I feel it is more obvious what Spring is doing.
Let’s take a look at how creating our own beans can help here.
Let’s look at what this new config class is doing. We have defined several beans that Spring will instantiate as singletons and add to its context. This gives Spring instances of Truck, Plane and Driver to give to whatever class has those fields.
Although it’s not obvious, this implementation also solves problem #2. The Driver in both the Plane and Truck beans is the same Driver instance! This may seem confusing because it looks like the driver() method was called twice and should then just create two new objects. However, Spring identifies that the method driver() creates a bean, and wires in the Driver object in its context instead. Isn’t that neat?
Additionally, we’ve eliminated “Bob” from our code base! Don’t worry, we can still find him in the application.properties file as
driverName=Boband, using the @Value annotation, Spring can inject his name in the driverName variable to be used in our application. Now, if we want to change up the driver, we don’t have to make any changes in the code itself, just the application.properties file.
We have now isolated properties and dependency management into the ShippingConfig. Meanwhile, our GroundShipping and AirShipping classes no longer have to worry about their vehicles’ configuration. Now that we have our vehicles available as beans, we can simplify our shipping classes like this.
When Spring sees the @Autowired annotation, it wires in the corresponding bean in its context. That easy! Now the shipping classes are in no way involved with the configuration of their dependencies, making them more loosely coupled and less vulnerable to change.
Ultimately, every class in our application tree will become a bean. By utilizing Spring for dependency injection, we can simplify our classes, separate the concerns of configuration and function, and sleep soundly knowing we aren’t wasting memory and performance to on-the-fly instantiation.
Let me know how you felt about this exercise and please leave feedback in the comments!
