Domain-Driven Design em projetos Rust: quando os tipos se tornam seus aliados
Você já escreveu uma aplicação Rust onde a lógica de negócio se afogava em um mar de structs, traits e macros? Sente aquela familiaridade quando, olhando para o código, você não consegue dizer de imediato o que determinada função faz no contexto do domínio? Se sim — bem-vindo ao clube. O Domain-Driven Design (DDD) visa resolver esse problema, e o Rust com seu sistema de tipos é o terreno ideal para implementar os princípios do DDD.
Neste artigo, não vamos recontar o livro de Evans. Em vez disso, mostrarei como aplicar Domain-Driven Design em projetos Rust com exemplos reais. Você aprenderá como, usando tipos-embrulho, arquitetura modular e structs expressivas, tornar o código de domínio limpo, seguro e compreensível. Vamos nessa!
Por que Rust e DDD são a dupla perfeita
Domain-Driven Design exige um isolamento rigoroso da lógica de domínio da infraestrutura. O Rust fornece ferramentas poderosas para isso:
- Sistema de tipos — permite modelar regras de negócio no nível do compilador. Um erro? O compilador não deixará você cometê-lo.
- Propriedade e empréstimo — garantem que agregados e entidades não sejam alterados acidentalmente em lugares inesperados.
- Sistema de módulos — separa claramente a camada de domínio, infraestrutura e aplicação.
Mas também existem armadilhas. O Rust não perdoa objetos "gordos" com um monte de estado mutável. Portanto, o DDD em Rust exige uma abordagem diferente para modelagem do que, digamos, em Java ou C#.
Ideia-chave: torne os estados inválidos irrepresentáveis
No DDD, buscamos que as regras de negócio estejam codificadas na própria estrutura de dados. No Rust, isso se transforma em um mantra: "Se um estado é inválido — torne impossível criá-lo".
Em vez de escrever:
struct User { name: String, email: String, age: u8,}Criamos tipos que já em tempo de compilação garantem a correção:
pub struct Email(String);pub struct Age(u8);pub struct UserName(String);
pub struct User { name: UserName, email: Email, age: Age,}Parece que são apenas wrappers? Na verdade, este é o primeiro passo para o DDD em Rust, onde os tipos se tornam documentação e guardiões dos invariantes.
Modelando o domínio: Value Object e Entity
Conceitos centrais do DDD — Entity (entidade) e Value Object (objeto de valor). No Rust, eles são implementados de forma diferente.
Value Object em Rust: imutabilidade e igualdade por valor
Value Object não possui identidade. Dois objetos são iguais se seus campos são iguais. Em Rust, encaixa-se perfeitamente com structs usando #[derive(PartialEq)] e campos imutáveis.
use std::fmt;
#[derive(Debug, Clone, PartialEq)]pub struct Money { amount: i64, currency: Currency,}
#[derive(Debug, Clone, PartialEq)]pub enum Currency { USD, EUR, RUB,}
impl Money { pub fn new(amount: i64, currency: Currency) -> Self { // Regra de negócio: dinheiro não pode ser negativo assert!(amount >= 0, "Amount must be non-negative"); Self { amount, currency } }
pub fn add(&self, other: &Money) -> Result<Self, String> { if self.currency != other.currency { return Err("Cannot add different currencies".into()); } Ok(Self::new(self.amount + other.amount, self.currency.clone())) }}Observe: Money é imutável. Qualquer operação retorna um novo objeto. Isso está alinhado com a filosofia do DDD em Rust: sem setters, apenas métodos que criam novas instâncias.