Dependency Injection in Rust: A Guide with Code Examples

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

Introduction to Dependency Injection in Rust



Dependency Injection (DI) is a design pattern where an object receives its dependencies from the outside rather than creating them itself. In Rust, where safety and performance are paramount, DI helps create flexible, testable, and loosely coupled code. In this article, we will explore how to implement dependency injection in Rust using traits, generics, and containers.



Unlike languages with reflection (Java, C#), Rust does not provide built-in DI frameworks. However, its type system and ownership model allow for elegant solutions without sacrificing performance.



Main Approaches to Dependency Injection



In Rust, there are several ways to implement DI. Let's look at three main approaches.



1. Constructor Injection



The simplest and most common way is to pass dependencies into the structure's constructor. This ensures that the object is always in a valid state.



// Define a trait for sending notificationstrait Notifier {    fn send(&self, message: &str);}

// Implementation for Emailstruct EmailNotifier { smtp_server: String,}

impl Notifier for EmailNotifier { fn send(&self, message: &str) { println!("Sending email via {}: {}", self.smtp_server, message); }}

// Implementation for SMSstruct SmsNotifier { api_key: String,}

impl Notifier for SmsNotifier { fn send(&self, message: &str) { println!("Sending SMS with key {}: {}", self.api_key, message); }}

// Service that uses Notifierstruct NotificationService { notifier: Box<dyn Notifier>,}

impl NotificationService { // Dependency injection through constructor fn new(notifier: Box<dyn Notifier>) -> Self { NotificationService { notifier } }

fn notify(&self, message: &str) { self.notifier.send(message); }}

fn main() { let email_notifier = Box::new(EmailNotifier { smtp_server: "smtp.example.com".to_string(), }); let service = NotificationService::new(email_notifier); service.notify("Hello, Rust!");}


2. Generic Injection



Using generics allows you to avoid dynamic dispatch and improve performance through monomorphization.



// Same Notifier trait, but the service is now genericstruct NotificationService<T: Notifier> {    notifier: T,}

impl<T: Notifier> NotificationService<T> { fn new(notifier: T) -> Self { NotificationService { notifier } }

fn notify(&self, message: &str) { self.notifier.send(message); }}

fn main() { let email_notifier = EmailNotifier { smtp_server: "smtp.example.com".to_string(), }; let service = NotificationService::new(email_notifier); service.notify("Hello from generic Rust!");}


3. Dependency Injection via Container



For complex projects, it is convenient to use a dependency container that manages the creation and lifetime of objects. In Rust, you can implement a simple container manually.



use std::collections::HashMap;

// Trait for dependency factoriestrait Factory<T> { fn create(&self) -> T;}

// Simple dependency containerstruct Container { factories: HashMap<String, Box<dyn std::any::Any>>,}

impl Container { fn new() -> Self { Container { factories: HashMap::new(), } }

fn register<T: 'static>(&mut self, name: &str, factory: Box<dyn Factory<T>>) { self.factories.insert(name.to_string(), Box::new(factory)); }

fn resolve<T: 'static>(&self, name: &str) -> Option<T> { self.factories.get(name).and_then(|f|

Blogs

Book Recommendations