Dependency Injection Containers in Rust: Overview and Implementation Using "dicy" as an Example

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Dependency Injection Container for Rust: Overview of the dicy Library



In the world of systems programming, Rust is renowned for its performance and memory safety. However, when building large applications, developers often face the challenge of managing dependencies. Manually wiring components leads to "tight coupling," complicates testing, and reduces flexibility. The solution is a Dependency Injection (DI) container, which automates the creation and injection of dependencies. In this article, we will explore the dicy library — one of the most mature DI containers for Rust.



What is Dependency Injection and Why Do You Need It in Rust?



Dependency Injection is a pattern where an object receives its dependencies from the outside, rather than creating them itself. In Rust, this is particularly relevant due to its strict type system and ownership model. A DI container takes on the following responsibilities:

  • Creating service instances (singleton, transient, scoped).
  • Resolving the dependency graph (recursive injection).
  • Managing the lifecycle of objects.
  • Allowing implementation substitution during testing (mock objects).


The dicy Library: Feature Overview



dicy is a lightweight yet powerful DI container written in pure Rust without unsafe code. It supports:

  • Automatic dependency resolution via constructors or factories.
  • Multiple lifecycle modes: Singleton, Transient, Scoped (for actix-web, axum, and other frameworks).
  • Async factories — creating services using async/await.
  • Trait-based injection — binding a trait to a specific implementation.
  • Nested containers (for module isolation).


Installation and Basic Setup



Add dicy to your Cargo.toml:

[dependencies]dicy = "0.6"


Example 1: Simple Constructor Injection



Let's create two services: Config (singleton) and Database (transient), which depends on Config.

use dicy::{Container, Module, Provide, Singleton, Transient};

#[derive(Clone)]struct Config { url: String,}

impl Config { fn new() -> Self { Config { url: "postgres://localhost".into() } }}

struct Database { config: Config,}

impl Database { fn new(config: Config) -> Self { Database { config } }}

// Define a module with registrationsstruct AppModule;

impl Module for AppModule { fn configure(container: &mut Container) { container .register::<Config>(Singleton::new(Config::new)) .register::<Database>(Transient::new(Database::new)); }}

fn main() { let container = Container::new(); container.install(AppModule);

// Get Database with Config automatically injected let db: Database = container.resolve().unwrap(); println!("Database connection: {}", db.config.url);}


Example 2: Async Factories and Scoped Dependencies



For web applications, it is often necessary to create services with an HTTP request scope (scoped). dicy supports this:

use dicy::{Container, Module, Scoped, AsyncFactory, Provide};use async_trait::async_trait;

struct RequestContext { user_id: u64,}

#[async_trait]trait UserService { async fn get_name(&self) -> String;}

struct UserServiceImpl { ctx: RequestContext,}

#[async_trait]impl UserService for UserServiceImpl { async fn get_name(&self) -> String { format!("User #{}", self.ctx.user_id) }}

struct WebModule;

impl Module for WebModule { fn configure(container: &mut Container) { container

Recommendations