Dependency Injection in Rust Using Waiter DI
By: Paolo Victor
Tags: Dependency Injection, Design Patterns, Rust, Software Architecture, Software Development

Photo credit: Lefteris kallergis

Dependency injection is a technique which is largely used to avoid tight coupling inside the applications, by abstracting objects/modules and inverting the dependency flow.

Of course this is a simplified explanation of it but, if you are here, I believe that we are familiarized with the technique and you might have googled “dependency injection in rust”, or something like that, in order to know specifically how to use it in Rust.

Otherwise, check this link out. The explanation about the topic is way more instructive =).

In this article I want to show how to use dependency injection using Waiter DI, a very useful DI crate for Rust.

Assuming that you have already installed Rust in your computer, select your workspace folder and run the following command:

cargo new di_poc --bin

A di_poc folder will be created. Open this folder inside of your preferred editor. I’m using VS Code.

The structure of the project is as follows:

Project structure

Inside of src folder, the following folders were created:

abst: where I put the trait (abstract code).

implementation: where I put the implementation, the struct which depends on the abstract.

services: the struct where the implementation for the trait will be injected

Inside of you cargo.toml file, include the following dependencies under the dependency section:

waiter_di = "1.6.2"

Waiter DI is the crate with the features we need in order to add the injection functionalities.

So far we have the Service that depends on the trait TUserRepo, which is the abstraction to the repository.

TUserRepo, the trait that abstracts the repository

UserService, the struct that has TUserRepo as a property

As TUserRepo is a trait, it must be implemented by another struct. UserService doesn’t know this concrete implementation, as it’s the rule when we’re talking about the dependency inversion principle. That’s the way we decouple these modules in order to be able to inject test mock classes or other implementations when needed without affecting the UserService (and all the other services) code.

Note that UserService struct has the module attribute. That’s how the container, called by the new() function knows that it has to inject something in it. Note that only by passing the service struct name to the container, it will identify the dependencies that have to be injected. In this case, the implementation of TUserRepo.

The new() function acts as a constructor, returning the struct itself. By using the container as mentioned before, the struct will be returned with the correct dependencies.

The service is also calling an injection container.

code to get the waiter_di container

If we have different profiles in the future, a centralized container would be an interesting idea, for example, to return different dependencies according to an environment variable indicating the type of execution (such as dev, test etc).

The UserRepo implementation is the following:

UserRepo struct

UserRepo struct implements TUserRepo trait. Note that it has component attribute and the implementation part has provides attribute, provided by waiter_di. These attributes are necessary to allow the waiter_di container to inject these implementation to the dependents of the trait which it implements.

To test this implementation, let’s create an instance of UserService and call the save() function.

main.rs file

cargo run

result of command execution

And the “user saved!” (version of “Hello Repository!”) was printed successfully, what means that our implementation was correctly injected and our service doesn’t even dream about this implementation.

The complete code is below:

That’s all, I hope you enjoy it. See you next time!