Diagrama conceitual de entidades e value objects em arquitetura de domínio

Sabe aquele momento em que você está modelando uma classe e fica na dúvida: "Essa classe precisa de um ID? Duas instâncias com os mesmos valores são iguais ou diferentes?" Se você já passou por isso, bem-vindo ao clube. Essa é uma das decisões mais importantes no DDD, e entender a diferença entre Entities e Value Objects pode transformar completamente a qualidade do seu código.

Vou te contar uma história real: há alguns anos, estava trabalhando em um sistema de e-commerce e criei uma classe Endereco com um campo Id. Parecia fazer sentido, afinal, todo objeto precisa de um ID, certo? Errado. Depois de alguns meses, o sistema estava cheio de bugs estranhos: endereços duplicados, comparações que não funcionavam direito, e um banco de dados poluído com milhares de registros de endereços idênticos mas com IDs diferentes.

O problema? Eu não tinha entendido a diferença fundamental entre uma Entity e um Value Object. E é exatamente isso que vamos desmistificar neste capítulo.

Hoje, você vai aprender:

  • O que são Entities e quando usá-las
  • O que são Value Objects e por que são fundamentais
  • Como implementar ambos em C# moderno
  • As diferenças práticas entre eles
  • Armadilhas comuns e como evitá-las
  • Padrões de implementação testados em produção

Entities: Objetos com identidade

O que define uma entity?

Imagine que você está em um hospital. Dois pacientes podem ter o mesmo nome, a mesma idade, até o mesmo endereço. Mas eles são pessoas diferentes, certo? Cada paciente tem uma identidade única - geralmente representada por um número de registro ou CPF.

Essa é a essência de uma Entity: um objeto que possui identidade única e continuidade ao longo do tempo. Mesmo que todos os seus atributos mudem, a identidade permanece a mesma.

Uma Entity é definida pela sua identidade, não pelos seus atributos. Duas entities com os mesmos valores de atributos mas IDs diferentes são objetos distintos.

Características de uma Entity

Uma Entity possui três características fundamentais:

Uma Entity tem três características fundamentais: ela possui uma identidade única, ou seja, cada instância tem um identificador que a distingue de todas as outras; mantém continuidade, porque essa identidade permanece a mesma durante todo o ciclo de vida do objeto; e é mutável, já que seus atributos podem mudar ao longo do tempo, mas a sua identidade em si não se altera.

As três características fundamentais que definem uma Entity no DDD

Implementando Entities

Uma Entity robusta combina identidade estável com comportamento de negócio e igualdade baseada em ID. Ela tem um identificador único (Id) que não muda, métodos que representam regras do domínio e comparação por identidade, não por referência ou por “todos os campos”.

Por exemplo, algo bem reduzido:

public abstract class Entity
{
    public Guid Id { get; protected set; } = Guid.NewGuid();    public override bool Equals(object? obj)
        => obj is Entity other && Id == other.Id;    public override int GetHashCode() => Id.GetHashCode();
}public class Pedido : Entity
{
    public decimal ValorTotal { get; private set; }
    public bool Confirmado { get; private set; }    public void Confirmar()
    {
        if (ValorTotal <= 0)
            throw new InvalidOperationException("Pedido sem valor não pode ser confirmado.");        Confirmado = true;
    }
}

A ideia é: o ID garante a identidade, e os métodos (Confirmar) carregam a regra de negócio dentro da própria Entity.

Por que este design é importante?

Vamos analisar os pontos-chave desta implementação:

1. Encapsulamento de identidade

public Guid Id { get; protected set; }

Id tem um set protegido, impedindo que seja alterado de fora da classe. A identidade é imutável uma vez criada.

2. Validações no construtor

public Pedido(string numeroPedido)
{
    if (string.IsNullOrWhiteSpace(numeroPedido))
        throw new ArgumentException("Número do pedido é obrigatório", nameof(numeroPedido));
    // ...
}

Uma Entity sempre inicia em um estado válido. Sem exceções.

3. Métodos de negócio, não Setters

public void ConfirmarPedido() { /* ... */ }
public void CancelarPedido() { /* ... */ }

Ao invés de expor setters que permitem qualquer mudança, criamos métodos que expressam operações do domínio e garantem invariantes.

4. Coleções protegidas

private readonly List<ItemPedido> _itens = new();
public IReadOnlyCollection<ItemPedido> Itens => _itens.AsReadOnly();

A coleção interna é privada. Externamente, só é possível ler. Mudanças acontecem apenas através de métodos controlados como AdicionarItem.

Se você se pegar criando propriedades públicas com setters em uma Entity, pare e pergunte: "Que operação de negócio estou realmente modelando aqui?" Transforme esse setter em um método com um nome significativo do domínio.

Value Objects: Objetos sem identidade

O que define um Value Object?

Agora vamos ao outro lado da moeda. Pense em uma nota de R$ 50. Se eu tenho uma nota de R$ 50 no bolso e você também tem uma, elas são iguais? Para fins práticos, sim. Não importa o número de série da nota, não importa se a minha está um pouco amassada - ambas representam o mesmo valor: cinquenta reais.

Essa é a essência de um Value Object: um objeto definido pelos seus atributos, não por uma identidade. Se dois Value Objects têm os mesmos valores, eles são considerados iguais.

Um Value Object é definido pelos seus atributos. Dois Value Objects com os mesmos valores são o mesmo objeto, independente de serem instâncias diferentes na memória.

Características de um Value Object

Um Value Object tem quatro características essenciais: ele não possui identidade própria, ou seja, não precisa de ID; é imutável, mantendo sempre os mesmos valores depois de criado; sua igualdade é definida pelos valores dos seus atributos; e ele é totalmente substituível, podendo ser trocado por outro com os mesmos dados sem causar qualquer impacto no sistema.

As quatro características que definem um Value Object no DDD

Exemplos clássicos de Value Objects

Um Value Object representa um conceito do domínio definido só pelos seus valores: não tem ID, é imutável e dois objetos com os mesmos atributos são considerados iguais. Coisas como dinheiro, endereço, e-mail, CPF, cor ou período de datas são exemplos clássicos: se os valores batem, é “o mesmo” para o domínio.

Um exemplo bem reduzido em C#:

public abstract class ValueObject
{
    protected abstract IEnumerable<object?> GetEqualityComponents();    public override bool Equals(object? obj)
        => obj is ValueObject other &&
           GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());    public override int GetHashCode()
        => GetEqualityComponents()
            .Select(x => x?.GetHashCode() ?? 0)
            .Aggregate((x, y) => x ^ y);
}public class Dinheiro : ValueObject
{
    public decimal Valor { get; }
    public string Moeda { get; }    public Dinheiro(decimal valor, string moeda)
    {
        Valor = valor;
        Moeda = moeda;
    }    protected override IEnumerable<object?> GetEqualityComponents()
    {
        yield return Valor;
        yield return Moeda;
    }
}

Aqui, dois Dinheiro(100, "BRL") são iguais, mesmo que sejam instâncias diferentes — porque o que importa é o valor e a moeda.

A Alternativa Moderna: Records

Com C# 9+, record é uma forma moderna e direta de criar Value Objects, porque já traz igualdade por valor “de fábrica”. Você foca na validação e na imutabilidade, e o compilador cuida do resto.

Um exemplo compacto com e-mail:

public record Email
{
    public string Valor { get; }    private Email(string valor) => Valor = valor;    public static Email Criar(string email)
    {
        if (string.IsNullOrWhiteSpace(email) || !email.Contains('@'))
            throw new ArgumentException("E-mail inválido", nameof(email));        return new Email(email.Trim().ToLowerInvariant());
    }    public override string ToString() => Valor;
}// Igualdade por valor:
var email1 = Email.Criar("JOAO@EMAIL.COM");
var email2 = Email.Criar("joao@email.com");
Console.WriteLine(email1 == email2); // True

A imutabilidade é crucial porque evita efeitos colaterais bizarros: se um Value Object pudesse ser alterado depois de criado, qualquer lugar que o esteja reutilizando poderia “mudar junto” sem querer. Quando ele é imutável, “mudar” significa criar uma nova instância, mantendo o resto do sistema previsível e seguro.

Value Objects imutáveis são thread-safe por natureza, podem ser compartilhados sem medo de efeitos colaterais, e facilitam o raciocínio sobre o código. Se um Value Object não pode mudar, você nunca terá surpresas desagradáveis.

Entity vs Value Object: Quando usar cada um?

Agora que vimos ambos, como decidir qual usar? Vou te dar um framework mental simples:

Faça estas perguntas:

1. "Este objeto precisa ser rastreado ao longo do tempo?"

  • Sim: Entity (ex: Pedido, Cliente, Produto)
  • Não: Value Object (ex: Endereço, Dinheiro, Email)

2. "Se dois objetos têm os mesmos atributos, eles são o mesmo?"

  • Sim: Value Object (ex: duas moedas de R$ 1 são iguais)
  • Não: Entity (ex: dois clientes com mesmo nome são pessoas diferentes)

3. "Este objeto pode mudar ao longo de sua vida?"

  • Sim, e preciso rastrear essas mudanças: Entity
  • Sim, mas crio um novo objeto a cada mudança: Value Object
  • Não: Definitivamente Value Object

4. "Faz sentido compartilhar este objeto entre múltiplas entities?"

  • Sim, sem problemas: Value Object
  • Sim, mas preciso saber quem está usando: Entity

Fluxo de decisão para escolher entre Entity e Value Object

Armadilhas comuns e como evitá-las

Depois de anos trabalhando com DDD, vi (e cometi!) muitos erros. Aqui estão os mais comuns:

Armadilha 1: Anemia de domínio

// ERRADO: Entity anêmica (apenas getters/setters)
public class Pedido : Entity
{
    public string Numero { get; set; }
    public DateTime Data { get; set; }
    public string Status { get; set; }
    public List<ItemPedido> Itens { get; set; }
    public decimal Total { get; set; }
}// A lógica fica espalhada em services
public class PedidoService
{
    public void ConfirmarPedido(Pedido pedido)
    {
        if (pedido.Status != "Novo")
            throw new Exception("Só pode confirmar pedidos novos");        if (pedido.Itens.Count == 0)
            throw new Exception("Pedido sem itens");        pedido.Status = "Confirmado";
        pedido.Total = pedido.Itens.Sum(i => i.Preco * i.Quantidade);
    }
}

CORRETO:

// Lógica de negócio dentro da Entity
public class Pedido : Entity
{
    public string Numero { get; private set; }
    public DateTime Data { get; private set; }
    public StatusPedido Status { get; private set; }
    private readonly List<ItemPedido> _itens = new();
    public IReadOnlyCollection<ItemPedido> Itens => _itens.AsReadOnly();
    public Dinheiro Total { get; private set; }    public void ConfirmarPedido()
    {
        if (Status != StatusPedido.Novo)
            throw new InvalidOperationException("Só pode confirmar pedidos novos");        if (!_itens.Any())
            throw new InvalidOperationException("Pedido sem itens");        Status = StatusPedido.Confirmado;
        RecalcularTotal();
    }    private void RecalcularTotal()
    {
        Total = _itens
            .Select(i => i.ValorTotal)
            .Aggregate((a, b) => a.Somar(b));
    }
}

Armadilha 2: Value Objects com setters

// ERRADO: Value Object mutável
public class Dinheiro : ValueObject
{
    public decimal Valor { get; set; } // NUNCA!
    public string Moeda { get; set; }  // NUNCA!
}// Problemas:
var preco1 = new Dinheiro { Valor = 100, Moeda = "BRL" };
var preco2 = preco1; // Mesma referênciapreco1.Valor = 50;
Console.WriteLine(preco2.Valor); // 50 - efeito colateral!

CORRETO:

// Value Object imutável
public class Dinheiro : ValueObject
{
    public decimal Valor { get; } // Somente get
    public string Moeda { get; }  // Somente get    private Dinheiro(decimal valor, string moeda)
    {
        Valor = valor;
        Moeda = moeda;
    }    public static Dinheiro Real(decimal valor) => new(valor, "BRL");    // Operações retornam NOVOS objetos
    public Dinheiro Somar(Dinheiro outro)
    {
        if (Moeda != outro.Moeda)
            throw new InvalidOperationException("Moedas diferentes");        return new Dinheiro(Valor + outro.Valor, Moeda);
    }
}

Armadilha 3: Obsessão por primitivos

// ERRADO: Usar tipos primitivos para conceitos de domínio
public class Cliente : Entity
{
    public string Email { get; set; } // String não valida formato
    public string CPF { get; set; }   // String não valida CPF
    public decimal Saldo { get; set; } // Decimal não tem moeda
}// Problemas:
cliente.Email = "email invalido"; // Aceita!
cliente.CPF = "abc"; // Aceita!
cliente.Saldo = -1000; // Saldo negativo?

CORRETO:

// Value Objects encapsulam validação e significado
public class Cliente : Entity
{
    public Email Email { get; private set; }
    public CPF CPF { get; private set; }
    public Dinheiro Saldo { get; private set; }    public void AtualizarEmail(Email novoEmail)
    {
        // Email já foi validado no construtor
        Email = novoEmail;
    }    public void Creditar(Dinheiro valor)
    {
        if (valor.Valor <= 0)
            throw new ArgumentException("Valor deve ser positivo");        Saldo = Saldo.Somar(valor);
    }
}// Uso:
var email = Email.Criar("joao@email.com"); // Valida no momento da criação
var cpf = CPF.Criar("123.456.789-00"); // Valida no momento da criação
var cliente = new Cliente(email, cpf);

Próximos Passos

No próximo capítulo, vamos mergulhar em Aggregates e Aggregate Roots - como agrupar Entities e Value Objects relacionados e garantir consistência através de fronteiras transacionais. Você vai aprender:

  • O que são Aggregates e por que são fundamentais
  • Como definir fronteiras de Aggregates
  • Implementando Aggregate Roots em C#
  • Gerenciando relacionamentos entre Aggregates
  • Garantindo consistência eventual

Entities e Value Objects são os blocos de construção, mas Aggregates são a cola que mantém tudo junto de forma consistente e escalável.

Até lá, pratique identificando Entities e Value Objects no seu domínio. Olhe para suas classes atuais e pergunte: "Isso precisa de identidade?" A resposta vai guiar você para um design muito mais robusto.

Referências e leituras complementares

Para aprofundar seu conhecimento sobre Entities e Value Objects:

  • Domain-Driven Design: Tackling Complexity in the Heart of Software - Eric Evans (o livro azul) O clássico que definiu DDD. Capítulos 5 e 6 cobrem Entities e Value Objects em profundidade.
  • Implementing Domain-Driven Design - Vaughn Vernon (o livro vermelho) Implementação prática de DDD. Capítulos 5 e 6 têm exemplos excelentes em várias linguagens.
  • Domain-Driven Design Distilled - Vaughn Vernon Versão condensada e mais acessível. Ótimo para revisão rápida dos conceitos.
  • Artigo: "Value Objects Explained" por Martin Fowler https://martinfowler.com/bliki/ValueObject.html Explicação concisa e clara sobre Value Objects.
  • Artigo: "Entities vs Value Objects" por Vladimir Khorikov https://enterprisecraftsmanship.com/posts/entity-vs-value-object-the-ultimate-list-of-differences/ Lista completa de diferenças práticas.

Lembre-se: Entities e Value Objects não são apenas padrões técnicos - eles são ferramentas para modelar seu domínio de forma que reflita a realidade do negócio. Use-os para capturar a essência do que você está construindo, não apenas para organizar dados.

Boa codificação, e nos vemos no próximo capítulo!

Compartilhe este artigo

Quer modernizar seu sistema?

Saiba mais sobre como modernizar suas aplicações e escalar seu negócio com tecnologia de ponta.

Fale com um especialista
Felipe Marciano

Sobre o Autor

Felipe Marciano

Felipe Marciano é um desenvolvedor apaixonado por tecnologia, especializado em .NET Core, Angular e soluções cloud-native. Com mais de 12 anos de experiência, dedica-se à modernização de sistemas legados e à arquitetura de microsserviços, sempre priorizando código limpo, boas práticas e soluções realmente escaláveis. Felipe busca inovação constante em novas ferramentas e frameworks para garantir alta qualidade e ótima experiência do usuário em cada projeto que lidera.

Posts Recomendados