Dependency Injection Containers for Rust: Tools and Practices Overview

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 Containers for Rust: Managing Dependencies in Systems Programming



In the world of systems programming, Rust is renowned for its performance and memory safety. However, as a project grows, the challenge of managing complex dependency graphs arises. Unlike languages with automatic memory management (Java, C#, Python), Rust requires a special approach to Dependency Injection (DI). In this article, we will explore what tools and libraries exist for implementing DI containers in Rust, and how to use them correctly.



Why Do You Need a DI Container in Rust?



The Dependency Injection pattern allows separating object creation from their usage. In Rust, this is especially relevant due to the strict ownership and borrowing system. A DI container solves several tasks:



  • Simplified testing — the ability to replace real implementations with mocks
  • Reduced code coupling — components depend on abstractions, not concrete implementations
  • Centralized lifecycle management — singletons, temporary objects, pools
  • Automatic dependency resolution — the container builds the object graph itself


Main Libraries for DI in Rust



1. shaku — Simple and Efficient DI Container



One of the most popular libraries. Based on traits and macros. Allows registering components with different lifetimes.



// Cargo.toml// [dependencies]// shaku = "0.6"


use shaku::{module, HasComponent, Interface};use std::sync::Arc;

// Define the interfacetrait Logger: Interface { fn log(&self, msg: &str);}

// Implementation#[derive(Clone)]struct ConsoleLogger;impl Logger for ConsoleLogger { fn log(&self, msg: &str) { println!("LOG: {}", msg); }}

// Registration modulemodule! { MyModule { components = [ConsoleLogger], providers = [] }}

// Usagefn main() { let module = MyModule::builder().build(); let logger: Arc<dyn Logger> = module.resolve().unwrap(); logger.log("Hello from DI!");}


2. dyn-clone + Manual Container



If you prefer minimalism, you can create a simple container manually using Any and dyn-clone. This gives you full control over the process.



use std::any::Any;use std::collections::HashMap;

struct SimpleContainer { services: HashMap<String, Box<dyn Any>>,}

impl SimpleContainer { fn new() -> Self { SimpleContainer { services: HashMap::new() } }

fn register<T: 'static>(&mut self, service: T) { let type_name = std::any::type_name::<T>().to_string(); self.services.insert(type_name, Box::new(service)); }

fn resolve<T: 'static>(&self) -> Option<&T> { let type_name = std::any::type_name::<T>().to_string(); self.services.get(&type_name) .and_then(|s| s.downcast_ref::<T>()) }}


3. once_cell and lazy_static — For Singletons



Often DI boils down to managing global states. These libraries help create lazily initialized singletons.



use once_cell::sync::Lazy;use std::sync::Mutex;

static CONFIG: Lazy<Mutex<Config>> = Lazy::new(|| { Mutex::new(Config::load())});

struct Config { db_url: String,}

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


Architectural Patterns for DI in Rust



In Rust, Constructor Injection is often used — dependencies are passed through the struct constructor. This is the most idi

Recommendations