Langsung ke konten utama
RekayasaMar 28, 2026

Dependency Injection di Rust — ServiceLocator, Arc, dan Trait Object

OS
Open Soft Team

Engineering Team

Dependency Injection di Rust

Rust tidak memiliki framework DI seperti Spring (Java) atau .NET. Tetapi sistem trait Rust menyediakan mekanisme DI yang kuat dan type-safe. Kuncinya: definisikan behavior sebagai trait, terima trait sebagai parameter, dan injeksi implementasi konkret saat runtime.

Trait sebagai Interface

#[async_trait]
pub trait UserRepository: Send + Sync {
    async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, DbError>;
    async fn create(&self, user: CreateUser) -> Result<User, DbError>;
    async fn update(&self, id: Uuid, data: UpdateUser) -> Result<User, DbError>;
    async fn delete(&self, id: Uuid) -> Result<(), DbError>;
}

#[async_trait]
pub trait EmailService: Send + Sync {
    async fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), EmailError>;
}

Implementasi Konkret

pub struct PgUserRepository {
    pool: PgPool,
}

#[async_trait]
impl UserRepository for PgUserRepository {
    async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, DbError> {
        sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
            .bind(id)
            .fetch_optional(&self.pool)
            .await
            .map_err(DbError::from)
    }
    // ... implementasi lainnya
}

pub struct SmtpEmailService {
    host: String,
    port: u16,
}

#[async_trait]
impl EmailService for SmtpEmailService {
    async fn send(&self, to: &str, subject: &str, body: &str) -> Result<(), EmailError> {
        // Kirim email via SMTP
        Ok(())
    }
}

Service Layer dengan DI

pub struct UserService<R: UserRepository, E: EmailService> {
    repo: R,
    email: E,
}

impl<R: UserRepository, E: EmailService> UserService<R, E> {
    pub fn new(repo: R, email: E) -> Self {
        Self { repo, email }
    }
    
    pub async fn register(&self, data: CreateUser) -> Result<User, AppError> {
        let user = self.repo.create(data).await?;
        self.email.send(
            &user.email,
            "Selamat datang!",
            "Terima kasih telah mendaftar."
        ).await?;
        Ok(user)
    }
}

Arc untuk Shared Ownership

Dalam aplikasi web, service sering di-share antar request handler. Arc memungkinkan shared ownership yang thread-safe:

type DynUserRepo = Arc<dyn UserRepository>;
type DynEmailService = Arc<dyn EmailService>;

#[derive(Clone)]
pub struct AppState {
    pub user_repo: DynUserRepo,
    pub email_svc: DynEmailService,
}

impl AppState {
    pub fn new(pool: PgPool, smtp_config: SmtpConfig) -> Self {
        Self {
            user_repo: Arc::new(PgUserRepository { pool: pool.clone() }),
            email_svc: Arc::new(SmtpEmailService {
                host: smtp_config.host,
                port: smtp_config.port,
            }),
        }
    }
}

impl Trait vs dyn Trait

impl Trait (static dispatch):

fn process(repo: &impl UserRepository) { ... }
// Dikompilasi menjadi fungsi terpisah per tipe konkret
// Zero-cost abstraction, tapi tidak bisa menyimpan tipe berbeda

dyn Trait (dynamic dispatch):

fn process(repo: &dyn UserRepository) { ... }
// Menggunakan vtable untuk dispatch saat runtime
// Fleksibel, overhead minimal (~2ns per panggilan)

Gunakan dyn Trait ketika:

  • Menyimpan di struct sebagai field
  • Menyimpan di koleksi heterogen
  • Memerlukan dependency injection saat runtime

Gunakan impl Trait ketika:

  • Parameter fungsi dengan tipe yang diketahui saat kompilasi
  • Return type fungsi
  • Performa kritis (menghindari vtable lookup)

Testing dengan Mock

#[cfg(test)]
mod tests {
    use super::*;
    
    struct MockUserRepo {
        users: Mutex<Vec<User>>,
    }
    
    #[async_trait]
    impl UserRepository for MockUserRepo {
        async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, DbError> {
            let users = self.users.lock().unwrap();
            Ok(users.iter().find(|u| u.id == id).cloned())
        }
        async fn create(&self, data: CreateUser) -> Result<User, DbError> {
            let user = User {
                id: Uuid::new_v4(),
                email: data.email,
                name: data.name,
            };
            self.users.lock().unwrap().push(user.clone());
            Ok(user)
        }
        // ...
    }
    
    struct MockEmailService {
        sent: Mutex<Vec<(String, String)>>,
    }
    
    #[async_trait]
    impl EmailService for MockEmailService {
        async fn send(&self, to: &str, subject: &str, _body: &str) -> Result<(), EmailError> {
            self.sent.lock().unwrap().push((to.to_string(), subject.to_string()));
            Ok(())
        }
    }
    
    #[tokio::test]
    async fn test_register_sends_email() {
        let repo = MockUserRepo { users: Mutex::new(vec![]) };
        let email = MockEmailService { sent: Mutex::new(vec![]) };
        let service = UserService::new(repo, email);
        
        let user = service.register(CreateUser {
            email: "test@example.com".into(),
            name: "Test User".into(),
        }).await.unwrap();
        
        assert_eq!(user.email, "test@example.com");
        // Verifikasi email dikirim
        let sent = service.email.sent.lock().unwrap();
        assert_eq!(sent.len(), 1);
        assert_eq!(sent[0].0, "test@example.com");
    }
}

Kesimpulan

Dependency injection di Rust menggunakan trait sebagai interface, Arc untuk shared ownership, dan dyn Trait untuk dispatch runtime. Pola ini memberikan testability tinggi tanpa framework DI eksternal — memanfaatkan kekuatan sistem tipe Rust untuk keamanan kompilasi dan zero-cost abstraction.