DEV Community

Sergio Marcial
Sergio Marcial

Posted on

Breaking Up with @Autowired: Why Dependency Injection with Spring Boot Needs a Makeover! πŸ’”

Introduction:
🎢 Love hurts, but sometimes love smokes your codebase! 🎢 let's talk about why the beloved @Autowired annotation in Spring Boot might not be the best match for your dependency injection needs. In this article, we'll explore the downsides of using @Autowired, discuss its impact on your codebase, and present alternative strategies to keep your codebase clean and healthy. Let's dive in! 🏊

  1. πŸ˜• What's Wrong with @Autowired?
    First things first! The @Autowired annotation might initially seem like a match made in heaven, promising effortless dependency injection in your Spring Boot projects. However, relying excessively on @Autowired can lead to several issues, turning it into a coding smell. Let's break them down:

  2. β›ˆοΈ Tight Coupling Alert!
    One major drawback of @Autowired is that it tightly couples your components, creating a web of dependencies that are difficult to trace and maintain (not that other forms of dependency injection not, they are just easier to identify). As your codebase grows, determining where exactly a dependency is injected becomes a daunting task. Take the following example:

@Component
public class ServiceA {
    @Autowired
    private ServiceB serviceB;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Here, the ServiceA is directly dependent on the ServiceB, severely limiting the flexibility of your code. Changes to the ServiceB or adding alternative implementations can potentially lead to cascading changes throughout your codebase. 😱

  1. πŸ™Š Hidden Dependencies When utilizing @Autowired, you might encounter instances where dependencies remain unclear or hidden. This creates a veil of uncertainty, making it harder for developers to understand the internal workings and requirements of components they are interacting with. Consider this example:
@Component
public class ShoppingCartService {
    @Autowired
    private PaymentService paymentService;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Without explicitly declaring the requirement for a PaymentService in the constructor or through method parameters, new developers ramping up in your code might struggle to grasp the essential dependencies of the ShoppingCartService or miss them completely. πŸ‘€

  1. πŸ”’ Testing becomes a Nightmare Writing test cases becomes increasingly complex when using @Autowired extensively. In order to effectively test a single component, you often end up dragging in multiple dependencies and configuring an elaborate setup. This makes test cases fragile and difficult to maintain. Take a look at this example:
@SpringBootTest
class CustomerServiceTest {
    @Autowired
    private CustomerService customerService;

    @Autowired
    private OrderService orderService;

    @Autowired
    private PaymentService paymentService;

    // ...
}
Enter fullscreen mode Exit fullscreen mode

In this scenario, testing the CustomerService becomes tedious due to the cascading dependencies mocking required for the successful execution of tests. πŸ˜“

πŸ› οΈ A Clean & Healthy Alternative:

Constructor Injection

So, how can we overcome these challenges? πŸ‘‰ Constructor Injection to the rescue! Constructor Injection promotes explicit declaration of dependencies and allows for clearer code comprehension. Let's see how the refactored examples look:

@Component
public class ServiceA {
    private final ServiceB serviceB;

    public ServiceA(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
    // ...
}

@Component
public class ShoppingCartService {
    private final PaymentService paymentService;

    public ShoppingCartService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }
    // ...
}
Enter fullscreen mode Exit fullscreen mode

By using constructor injection, we eliminate surprises and make dependencies explicit, leading to cleaner and more maintainable code. 🧹

Conclusion:
In this article, we mentioned some of the downsides of relying excessively on Spring Boot's @Autowired annotation for dependency injection. We explored the challenges it poses, such as tight coupling, hidden dependencies, and testing complexities. Fortunately, we present an elegant solution - constructor injection. Embrace constructor injection, and you'll foster codebases that are clean, comprehensible, and hassle-free to test! πŸŽ‰

Remember, while it's okay to have a fling with @Autowired early on in your coding adventures, it's crucial to break up after the honeymoon phase ends to ensure a long-lasting, healthy relationship with your codebase. Happy coding! πŸ’»πŸš€

Top comments (4)

Collapse
 
moaxcp profile image
John Mercier

This does not discuss a problem with spring boot or @AutoWired as the title implies. @Autowired can be used on fields, constructors or setters. With only one constructor you can remove @Autowired altogether.

The main issue discussed in this article is in using field injection vs constructor injection. Both are supported in spring and other dependency injection frameworks.

Personally, I don't mind using either one. Once you figure out it is a choice of style and how each choice works, it is pretty easy to follow each style.

Tight coupling is unrelated to dependency injection style and can happen in either case.

Hidden dependency is a minor problem in most cases. Intellij or any good ide will still link the dependencies and provide navigation.

For testing there is no difference. Mock frameworks provide field injection. The mocks will be injected in both cases just fine. You should still avoid mocks when possible.

Really it is just a different style with one small benefit of not requiring a constructor or setters.

Collapse
 
dagnelies profile image
Arnaud Dagnelies • Edited

I think you completely miss the point of @Autowired with oversimplified examples. The important missed thing is the "scope". While some components are singletons, others have a "@SessionScope" and others have a "@RequestScope". And that's where the magic is. It doesn't matter where that component is autowired, nor its scope, it will be adequately populated. Otherwise, you'd need to instanciate a component depending on the request and session, then manually inject it everywhere it belongs. That would be hell!

To take the example you provided, you'll likely not autowire ShoppingCartService, that would be your "RestController". What you would autowire is a ShoppingCart which contains the items your user picked. And this ShoppingCart will be available for all your services, with the right items in it.

Collapse
 
murphy2134 profile image
murphy1312

When would you ever invoke the constructor of ShoppingCartService manually?
yeah, you don't..
So while I also changed my codebases, just to satisfy sonarlint, I don't really see the point here.

Collapse
 
sergiomarcial profile image
Sergio Marcial • Edited

That is a really good point, but is more than satisfy sonarlint, it is about creating code that is more self explanatory, which is the point of the constructor, it helps to reduce the coginitive load of having to look at the entire class to see what dependencies need.

One example which I recentrly faced was when creating unit tests in that case you don't have an application context, with @Autowiring you need to modify assign values to variables after you create the class, but when using the construcutre of class to test it you create the class with the necessary depdencies from the start.

while might mostly a matter of style of some cases, it can help the code base in the long run