Executando verificação de segurança...
11

Arquitetura de Software em C# no Mundo Real: Capítulo 8 – Domínio expressivo: Value Objects, Invariantes e Eventos de Domínio

Essa série de artigos nasceu da frustração — e da prática.

Se você já tentou organizar um sistema .NET complexo, sabe que não é fácil: cada time tem uma visão diferente, os tutoriais se contradizem, e os “padrões” muitas vezes atrapalham mais do que ajudam. Entre um GenericRepository<T> e um PaymentServiceHandlerProcessorFactory, fica difícil saber o que realmente importa.

Aqui, a proposta é simples: falar sobre arquitetura real, com um olhar crítico, direto e sem medo de questionar modinhas. Vamos explorar estilos, padrões, anti-padrões e armadilhas comuns que se acumulam ao longo dos anos — e mostrar alternativas que funcionam fora do slide de apresentação.

Essa série é para desenvolvedores que já sujaram as mãos. Que já herdaram legado, quebraram produção, e tentaram explicar DDD em reunião de grooming.

Sem fórmulas mágicas. Sem purismo. Só código, contexto e experiência real.


Índice da Série

Capítulo 1 – Os caminhos que a arquitetura pode tomar

Um panorama direto e sem rodeios sobre Transaction Script, Modelo Anêmico, Modelo Rico e DDD.

Capítulo 2 – O modelo anêmico vai te trair (e como saber quando fugir dele)

Quando usar um modelo anêmico parece mais simples, mas te afunda em bugs e complexidade acidental.

Capítulo 3 – A falsa promessa do “service pattern” genérico

AccountService, UserManager, FooHandler: nomes que escondem baixa coesão e design acoplado.

Capítulo 4 – Entidades ricas: encapsulamento é mais que private set;

Como construir entidades com comportamento real e proteger sua integridade.

Capítulo 5 – A camada de aplicação não é um lixo eletrônico

Application Services devem orquestrar, não conter regras de negócio nem virar dumping ground de dependências.

Capítulo 6 – Use Cases, Command Handlers e MediatR: o que separa clareza de confusão

Quando aplicar CQRS traz benefícios reais — e quando só adiciona complexidade desnecessária.

Capítulo 7 – Repositórios que respeitam o domínio (e evitam vazamento de infra)

Um repositório não é só um wrapper de DbContext. É um contrato do domínio com a persistência.

Capítulo 8 – Domínio expressivo: Value Objects, Invariantes e Eventos de Domínio

Modelando regras com segurança e intenção. O domínio deve falar por si.

Capítulo 9 – Dividindo para conquistar: a importância dos Bounded Contexts

DDD real exige saber onde termina um modelo e começa outro. E como manter essa separação.

Capítulo 10 – Anti-patterns arquiteturais que todo dev .NET já viu (ou cometeu)

Um olhar honesto sobre os maiores pecados que já cometemos em nome da “arquitetura”.

Capítulo 11 – Mitos e Más Interpretações na Arquitetura .NET

Por que MVC não é arquitetura, repositório não é para toda entidade, padrão não é enfeite, e SOLID não é carta branca pra travar refatoração.

Capítulo 8 – Domínio expressivo: Value Objects, Invariantes e Eventos de Domínio

Chegamos ao ponto em que o código já tem cara de domínio: entidades fazem sentido, repositórios não cheiram mais a genérico, e handlers finalmente parecem scripts de orquestração. Mas falta algo...

Falta o domínio falar.

Falta abrir uma classe e saber de cara o que ela representa, o que ela permite, e o que ela nunca aceitará.

Falta o modelo ser mais que uma coleção de public string, public decimal, public bool.

É aqui que entram os Value Objects, as invariantes e os eventos de domínio.


Value Objects: encapsular é libertar

Vamos começar por onde a maioria erra: o decimal Amount.

Toda aplicação financeira tem isso. E quase toda trata como um tipo primitivo em toda parte.

Mas pense comigo: Amount é só um número?

  • Ele precisa ser positivo?
  • Ele pode ter casas decimais?
  • Ele representa o valor em qual moeda?
  • Ele pode ser somado com outro valor de moeda diferente?

Se essas regras existem… por que o Amount ainda é um decimal?

A resposta: porque você ainda não modelou o domínio.


Um bom Value Object carrega significado, invariantes e semântica

public record Money
{
    public decimal Value { get; }
    public string CurrencyCode { get; }

    public Money(decimal value, string currencyCode)
    {
        if (value < 0)
            throw new ArgumentException("Value must be non-negative");

        if (string.IsNullOrWhiteSpace(currencyCode))
            throw new ArgumentException("Currency is required");

        Value = value;
        CurrencyCode = currencyCode.ToUpperInvariant();
    }

    public Money Add(Money other)
    {
        if (other.CurrencyCode != CurrencyCode)
            throw new InvalidOperationException("Cannot add amounts with different currencies");

        return new Money(Value + other.Value, CurrencyCode);
    }
}

Agora seu domínio não é só mais seguro.
Ele explica sua regra com o próprio código.


Invariantes: as regras que nunca mudam

Toda entidade tem regras que não podem ser quebradas. Jamais.
Se elas forem violadas, o sistema está corrompido.

Essas são as invariantes.

Exemplos:

  • Uma conta não pode ser fechada se ainda tiver saldo.
  • Um trade não pode ser aprovado duas vezes.
  • Um pagamento não pode ser marcado como “paid” sem uma instrução de liquidação válida.

Você pode deixar essas regras espalhadas em ifs ao longo do código...

Ou você pode colocá-las no domínio, guardadas a sete chaves:

public void Approve()
{
    if (Status != TradeStatus.PendingApproval)
        throw new DomainException("Only pending trades can be approved.");

    Status = TradeStatus.Approved;
}

Agora, não importa se o comando vem da UI, de um worker, de um ETL ou de um fluxo de eventos.
A regra é a mesma. Está no lugar certo.


Eventos de domínio: reações naturais de um modelo vivo

Outro ponto negligenciado: eventos de domínio.

Se o modelo representa comportamento e estado, ele também reage a mudanças.
E essas reações precisam ser capturadas.

Exemplo: ao aprovar um pagamento, talvez você precise:

  • Notificar compliance
  • Gerar instrução de liquidação
  • Disparar alerta de risco

Essas não são responsabilidades diretas da entidade.
Mas a entidade sabe que algo relevante aconteceu.

Logo, ela emite um evento:

public void Approve()
{
    if (Status != PaymentStatus.Pending)
        throw new DomainException("Only pending payments can be approved.");

    Status = PaymentStatus.Approved;

    _domainEvents.Add(new PaymentApproved(Id, Amount, AccountNumber));
}

E depois, no seu pipeline de Unit of Work ou MediatR, esses eventos são despachados.

Isso torna seu sistema mais reativo, coeso e observável — sem acoplamento.


🧪 Testando tudo isso

Com Value Objects e invariantes bem definidos:

  • Testar o domínio fica fácil: você instancia e interage.
  • Testar regras de negócio vira algo natural.
  • Você evita mocks e stubs em 80% dos testes.
  • Você começa a se sentir confortável com mudanças, porque o modelo está blindado contra inputs inválidos.

Conclusão

Não subestime o poder de modelar bem.

  • Value Objects encapsulam significado.
  • Invariantes protegem a integridade.
  • Eventos de domínio tornam o modelo expressivo e reativo.

Quando seu domínio fala com clareza,
a aplicação vira uma orquestra — e não um monte de banda tocando sozinha.


Próximo capítulo:
E se vamos falar de expressividade e contexto, o próximo capítulo precisa ser sobre Bounded Contexts
como separar responsabilidades, linguagens e modelos em sistemas complexos sem se perder.

Carregando publicação patrocinada...