DEV Community

Cover image for The ultimate Dependency Injection guide you need to read
Raffael Eloi
Raffael Eloi

Posted on

The ultimate Dependency Injection guide you need to read

Dependency injection has been one of the most frequently asked topics in job interviews, and I’ve seen it increase since the rise of AI.

I really believe that since the rise of AI, some companies have started filtering for candidates with stronger software engineering foundations.

The tricky thing is that sometimes you can use DI (Dependency Injection) daily at work, but you freeze when trying to explain the concepts and how to use it properly. That’s why I will try to help you out, and explain to you how it works, why it was created and when to use each injection scope in a way that will stick in your mind, so you can do better in your next interview or even become a better software engineer.

What is dependency injection?

Before adding the definition, have you ever wondered why we need to inject dependencies? What are the benefits? Is that the only way to do it? What are the drawbacks of not using DI?

The short answer is that you don’t necessarily need to use DI, you can build software by adding its dependencies manually, even though I strongly discourage it.

The world without DI

Bear with me. When class A depends on class B, you need to make sure that class B is instantiated when you call it from class A, otherwise you will get a null reference exception.

This is easy to do, don’t get me wrong, but you need to be aware that you will be the one responsible for instantiating all the dependencies, so that when a class invokes its dependency, it can work smoothly.

That is the main point: you are responsible for instantiating the dependencies and making sure each dependency lives as long as it should and is disposed at the right time, among other things.

Drawbacks of not using DI

  • You are responsible for taking care of all the dependencies.
  • It is hard to test, since you have to manually instantiate the dependencies inside the class.
  • If a dependency class changes its constructor, all the consumers need to change as well.
  • Without DI, it’s much harder to follow the Dependency Inversion Principle (the “D” in SOLID), so modules tend to end up tightly coupled.

The world with DI

Ok, now we can talk about the DI definition and its benefits.

“Dependency injection is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.”

In practice, a class declares what it needs through its constructor, and something else provides it, instead of the class creating its own dependencies.

Here is the biggest difference: you can use the “inversion of control” approach instead of manually adding the application dependencies.

What is inversion of control?

“Inversion of Control (IoC) is a software design principle where a framework, container, or system takes charge of the program’s flow of execution or object creation, rather than your custom application code calling the system directly.”

When you use IoC in your software, you can use a mechanism to inject the application’s dependencies for you according to the lifetime you choose. Once you set up all the necessary configuration, your application will create and dispose of the objects automatically, according to what you defined.

Understanding Service Lifetime

There are three lifetime types: scoped, transient and singleton. You can choose how you inject your classes by registering them with one of these lifetimes. This is important because you can control the instantiation of your services, repositories, database connections, credential services and so on.

Scoped

In .NET Core, a scope = one HTTP request.

It creates one instance per request (scope), so every class that asks for the service during that request gets the same instance.

The next request gets a new instance.

Following best practices, you can use it to inject your services, use cases, repositories and DbContexts.

Singleton

You have only one instance for the whole app.

Once the application starts and instantiates a class using the singleton lifetime, the same instance will be used for all subsequent requests.

Keep in mind that a singleton is shared across all requests and threads, so it must be thread-safe.

Following best practices, you can use it to inject your cache configs, credential services and background services that keep running for the entire application lifecycle.

Transient

It creates a new instance every time it is resolved.

It is commonly used for very lightweight and stateless classes.

You should use it carefully, because it is not reused by the whole application or by the request scope, so don’t use it for expensive objects to avoid wasting resources.

Be careful with disposable transients as well: the container keeps track of every disposable object it creates and only releases it when its scope ends. If you resolve one from the root provider, that “scope” is the whole application, so the object stays in memory until the app shuts down. Always resolve it inside a short-lived scope.

Following best practices, you can use it to inject your mappers and other very lightweight classes.

Benefits of DI

  • Keeps the code loosely coupled
  • Makes testing easier, because you can inject mocks properly
  • Provides a centralized place to handle all the injections
  • Lets you control each class’s lifetime

That’s it! Once you understand the whole idea of dependency injection and keep in mind the purpose of each injection type, it is easier to make good decisions and memorize this definition.

I wrote this article myself and used AI only to review the technical details. I really wanted to make it fun to read instead of those boring and verbose articles we have nowadays. I hope you liked it.

Feel free to leave feedback about the post, either positive or negative, or share your opinion about the topic.

You can find me here:

https://raffaeleloi.dev

References

Top comments (0)