Skip to main content

Dependency Injection | Rust

  • Dependency Injection | Rust

Dependency Injection (DI) no Rust

O que é DI

Dependency Injection (DI) é um padrão onde uma função, struct ou módulo recebe suas dependências prontas, em vez de criá-las internamente.

Em outras palavras:

  • ✅ Quem usa uma dependência não deve criar
  • ✅ Quem cria, deve injetar para quem precisa usar

Por que DI é útil no Rust

DI ajuda a manter o código:

  • mais organizado
  • mais testável
  • com menos acoplamento
  • mais fácil de trocar implementações (ex: Postgres → Mock)

No Rust isso é especialmente importante porque você geralmente quer:

  • inicializar recursos pesados apenas uma vez (pool do banco, clients HTTP, configs)
  • reaproveitar esses recursos com segurança (clonando handles como PgPool)

Problema: criar dependências dentro do handler

Exemplo não recomendado (acoplado e ineficiente):

async fn health() -> String {
let pool = connect_db().await; // cria toda vez
"ok".to_string()
}

Principais problemas:

  • cria conexão/pool a cada request
  • piora performance
  • dificulta testar
  • mistura responsabilidade de infraestrutura com regra de negócio

DI por parâmetro de função

Esse é o tipo mais simples de DI.

fn calcular_total(taxa: f64) -> f64 {
100.0 * taxa
}

Aqui taxa é uma dependência “injetada” via argumento.


DI por struct (Services e Repositories)

Esse é o padrão mais comum em aplicações Rust “bem separadas por camadas”.

Exemplo:

use sqlx::PgPool;

pub struct UserService {
db: PgPool,
}

impl UserService {
pub fn new(db: PgPool) -> Self {
Self { db }
}

pub async fn ping(&self) -> Result<(), sqlx::Error> {
sqlx::query("SELECT 1")
.execute(&self.db)
.await?;

Ok(())
}
}

Características:

  • UserService depende do banco (PgPool)
  • o banco é recebido no new(...)
  • o service não sabe “como” o banco foi criado

DI em Actix-web com web::Data

No Actix, DI é feito via estado compartilhado de aplicação com web::Data<T>.

Você registra a dependência no App:

use actix_web::{web, App};
use sqlx::PgPool;

App::new()
.app_data(web::Data::new(pool.clone()))

E recebe automaticamente no handler:

use actix_web::{web, Responder};
use sqlx::PgPool;

async fn health(db: web::Data<PgPool>) -> impl Responder {
"ok"
}

Esse db: web::Data<PgPool> é DI porque:

  • o handler não cria PgPool
  • o Actix fornece a instância configurada
  • a dependência fica centralizada no main

Como o Actix injeta o PgPool

O fluxo padrão é:

  1. Você cria o PgPool uma vez no main
  2. Registra o pool como App State com web::Data
  3. O Actix detecta web::Data<PgPool> na assinatura do handler
  4. Ele injeta automaticamente o valor correto em cada requisição

Exemplo:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
let pool = connect_db().await;

HttpServer::new(move || {
App::new()
.app_data(web::Data::new(pool.clone()))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}

DI em camadas (Handler → Service)

Separar camadas melhora manutenção e testes.

Exemplo de handler usando service:

use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;

pub async fn health(db: web::Data<PgPool>) -> impl Responder {
let service = HealthService::new(db.get_ref().clone());

match service.check().await {
true => HttpResponse::Ok().body("ok"),
false => HttpResponse::InternalServerError().body("fail"),
}
}

Service:

use sqlx::PgPool;

pub struct HealthService {
db: PgPool,
}

impl HealthService {
pub fn new(db: PgPool) -> Self {
Self { db }
}

pub async fn check(&self) -> bool {
sqlx::query("SELECT 1")
.execute(&self.db)
.await
.is_ok()
}
}

DI com trait para facilitar testes

Quando você quer desacoplar de uma implementação específica, você pode usar trait.

Definindo uma interface:

#[async_trait::async_trait]
pub trait UserRepository {
async fn count_users(&self) -> i64;
}

Implementação real (Postgres):

use sqlx::PgPool;

pub struct PgUserRepository {
db: PgPool,
}

#[async_trait::async_trait]
impl UserRepository for PgUserRepository {
async fn count_users(&self) -> i64 {
10
}
}

Implementação fake (teste):

pub struct FakeUserRepository;

#[async_trait::async_trait]
impl UserRepository for FakeUserRepository {
async fn count_users(&self) -> i64 {
42
}
}

Nesse modelo, o service recebe dyn UserRepository por DI.


Padrões comuns de DI no Rust

As formas mais usadas são:

  • passar dependências por parâmetro de função
  • usar struct + new(...) para injetar dependências
  • usar web::Data<T> (em Actix) como container simples de estado
  • usar trait para permitir troca de implementação (mock, fake, postgres)

Boas práticas de DI para Postgres (SQLx)

Recomendado:

  • criar PgPool apenas uma vez no main
  • compartilhar o pool via web::Data<PgPool>
  • passar PgPool para services/repositories via new(...)
  • usar clone() do pool sem medo (é barato e seguro)
  • manter queries fora de handler quando possível

Erros comuns

Evite:

  • criar conexão/pool dentro de cada handler
  • misturar lógica HTTP com queries diretamente no handler
  • espalhar .env / env::var() em vários lugares do código
  • passar String DATABASE_URL ao invés do pool em todos os lugares