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 Also in library
Java Task Scheduler: Quartz Scheduler – Complete Guide
Behat PHP Guide: Complete Tutorial with Examples
SwiftyJSON for Swift: Complete Guide with Examples