DDD: Value Objects e Entities - Pilares do modelo de domínio (Parte 4)

DDD16 min de leitura

Este é o quarto artigo da série sobre Domain-Driven Design. Nos artigos anteriores, exploramos os conceitos fundamentais do DDD, a importância da Linguagem Ubíqua e como os Bounded Contexts nos ajudam a delimitar contextos. Agora vamos mergulhar nos blocos de construção mais fundamentais do modelo de domínio: Value Objects e Entities.

O que são Value Objects e Entities?

No coração de qualquer modelo de domínio rico estão dois tipos principais de objetos: Entities (Entidades) e Value Objects (Objetos de Valor). A distinção entre eles é fundamental para criar um modelo de domínio expressivo e bem estruturado.

A Questão da Identidade

A diferença fundamental entre Entities e Value Objects está na forma como determinamos sua identidade:

Entities são objetos que possuem uma identidade única que persiste ao longo do tempo. Mesmo que todos os seus atributos mudem, uma Entity continua sendo a mesma Entity se sua identidade permanecer.

Value Objects são definidos pelos seus atributos e não possuem identidade conceitual. Dois Value Objects com os mesmos atributos são considerados idênticos.

Como Eric Evans define no livro original de DDD:

"Muitos objetos não são fundamentalmente definidos por seus atributos, mas sim por um fio de continuidade e identidade."

Entities: Identidade e Ciclo de Vida

Características das Entities

  1. Identidade única: Cada Entity possui um identificador único que não muda durante sua vida útil
  2. Ciclo de vida: Entities são criadas, podem ser modificadas ao longo do tempo e eventualmente podem ser destruídas
  3. Rastreabilidade: É importante saber se duas instances representam a mesma Entity
  4. Mutabilidade: Entities podem ter seus atributos alterados mantendo sua identidade

Exemplos Práticos de Entities

Usuário em um sistema:

class Usuario
{
    private int $id;
    private string $nome;
    private string $email;
    private DateTimeImmutable $dataCriacao;
    
    public function __construct(int $id, string $nome, string $email)
    {
        $this->id = $id;
        $this->nome = $nome;
        $this->email = $email;
        $this->dataCriacao = new DateTimeImmutable();
    }
    
    // Um usuário mantém sua identidade mesmo se mudar nome ou email
    public function equals(Usuario $other): bool
    {
        return $this->id === $other->id;
    }
    
    public function alterarEmail(string $novoEmail): void
    {
        $this->email = $novoEmail;
    }
    
    public function getId(): int
    {
        return $this->id;
    }
}

Pedido em um e-commerce:

class Pedido
{
    private string $numeroPedido;
    private StatusPedido $status;
    private array $itens;
    private float $valorTotal;
    
    public function __construct(string $numeroPedido)
    {
        $this->numeroPedido = $numeroPedido;
        $this->status = StatusPedido::CRIADO;
        $this->itens = [];
        $this->valorTotal = 0.0;
    }
    
    // O pedido é a mesma entidade independente do status ou itens
    public function adicionarItem(ItemPedido $item): void
    {
        $this->itens[] = $item;
        $this->recalcularTotal();
    }
    
    public function equals(Pedido $other): bool
    {
        return $this->numeroPedido === $other->numeroPedido;
    }
    
    private function recalcularTotal(): void
    {
        $this->valorTotal = array_sum(
            array_map(fn($item) => $item->getSubtotal(), $this->itens)
        );
    }
}

Implementando Identidade

A identidade de uma Entity pode vir de diferentes formas:

Identificadores Naturais: Quando o domínio já possui um identificador único

  • CPF para Pessoa
  • CNPJ para Empresa
  • Código de barras para Produto

Identificadores Gerados: Quando não existe um identificador natural

  • UUIDs
  • IDs auto-incrementais
  • Códigos gerados com prefixos (ex: "USR-123456")

Value Objects: Descrição sem Identidade

Value Objects representam aspectos descritivos do domínio que não possuem identidade conceitual. Como Evans explica:

"Um objeto que representa um aspecto descritivo do domínio sem identidade conceitual é chamado de Value Object."

Características dos Value Objects

  1. Imutabilidade: Uma vez criados, não podem ser modificados
  2. Igualdade estrutural: Dois Value Objects com os mesmos atributos são idênticos
  3. Sem identidade: Não há necessidade de rastreá-los ao longo do tempo
  4. Funções side-effect free: Operações que não causam efeitos colaterais

Exemplos Práticos de Value Objects

Dinheiro:

final readonly class Dinheiro
{
    public function __construct(
        public float $valor,
        public Moeda $moeda
    ) {
        if ($valor < 0) {
            throw new InvalidArgumentException("Valor não pode ser negativo");
        }
    }
    
    public function somar(Dinheiro $outro): Dinheiro
    {
        if ($this->moeda !== $outro->moeda) {
            throw new InvalidArgumentException("Moedas diferentes");
        }
        return new Dinheiro($this->valor + $outro->valor, $this->moeda);
    }
    
    public function multiplicar(int $quantidade): Dinheiro
    {
        return new Dinheiro($this->valor * $quantidade, $this->moeda);
    }
    
    public function isMaiorQue(Dinheiro $outro): bool
    {
        if ($this->moeda !== $outro->moeda) {
            throw new InvalidArgumentException("Moedas diferentes");
        }
        return $this->valor > $outro->valor;
    }
    
    // Igualdade baseada nos atributos
    public function equals(Dinheiro $outro): bool
    {
        return $this->valor === $outro->valor && $this->moeda === $outro->moeda;
    }
    
    public static function reais(float $valor): self
    {
        return new self($valor, Moeda::BRL);
    }
}

enum Moeda: string
{
    case BRL = 'BRL';
    case USD = 'USD';
    case EUR = 'EUR';
}

Endereço:

final readonly class Endereco
{
    public function __construct(
        public string $logradouro,
        public string $numero,
        public string $cep,
        public string $cidade,
        public string $estado
    ) {
        $this->validarCep($cep);
        $this->validarCamposObrigatorios($logradouro, $cidade, $estado);
    }
    
    public function isEnderecoValido(): bool
    {
        return !empty($this->cep) && !empty($this->cidade) && !empty($this->estado);
    }
    
    public function equals(Endereco $outro): bool
    {
        return $this->logradouro === $outro->logradouro
            && $this->numero === $outro->numero
            && $this->cep === $outro->cep
            && $this->cidade === $outro->cidade
            && $this->estado === $outro->estado;
    }
    
    public function comNovoNumero(string $novoNumero): self
    {
        return new self(
            $this->logradouro,
            $novoNumero,
            $this->cep,
            $this->cidade,
            $this->estado
        );
    }
    
    private function validarCep(string $cep): void
    {
        if (!preg_match('/^\d{5}-?\d{3}$/', $cep)) {
            throw new InvalidArgumentException("CEP inválido: {$cep}");
        }
    }
    
    private function validarCamposObrigatorios(string ...$campos): void
    {
        foreach ($campos as $campo) {
            if (empty(trim($campo))) {
                throw new InvalidArgumentException("Campos obrigatórios não podem estar vazios");
            }
        }
    }
}

Como Decidir Entre Entity e Value Object

A decisão entre modelar algo como Entity ou Value Object depende do contexto e das necessidades do domínio. Aqui estão algumas perguntas que podem ajudar:

Perguntas para Entity

  1. "Preciso rastrear este objeto ao longo do tempo?"

    • Se sim, provavelmente é uma Entity
  2. "Duas instâncias com os mesmos atributos representam a mesma coisa?"

    • Se não, provavelmente é uma Entity
  3. "Este objeto tem um ciclo de vida importante para o negócio?"

    • Se sim, provavelmente é uma Entity

Perguntas para Value Object

  1. "Posso substituir este objeto por outro com os mesmos atributos?"

    • Se sim, provavelmente é um Value Object
  2. "Este objeto descreve uma característica de algo?"

    • Se sim, provavelmente é um Value Object
  3. "A igualdade é baseada nos valores dos atributos?"

    • Se sim, provavelmente é um Value Object

Exemplo: O Contexto Importa

Um Endereço pode ser tanto Entity quanto Value Object dependendo do contexto:

Como Value Object (na maioria dos casos):

  • Descreve onde uma pessoa mora
  • Dois endereços iguais são o mesmo endereço
  • Não há necessidade de rastrear o endereço individualmente

Como Entity (em um sistema de entregas):

  • Cada endereço tem um histórico de entregas
  • Precisa rastrear tentativas de entrega
  • Dois endereços iguais podem ter históricos diferentes

Combatendo a Obsessão por Primitivos

Value Objects são uma ferramenta poderosa para combater o que conhecemos como "Primitive Obsession" - o uso excessivo de tipos primitivos para representar conceitos de domínio.

Problema: Método com muitos primitivos

// Problemático: muitos string e int
function criarUsuario(
    string $nome, 
    string $email, 
    string $cep,
    string $cidade, 
    string $estado, 
    int $idade
): void {
    // Validações espalhadas
    if (empty($nome)) {
        throw new InvalidArgumentException("Nome inválido");
    }
    if (!str_contains($email, '@')) {
        throw new InvalidArgumentException("Email inválido");
    }
    // ... mais validações
}

Solução: Value Objects expressivos

// Melhor: Value Objects expressivos
function criarUsuario(Nome $nome, Email $email, Endereco $endereco, Idade $idade): void
{
    // Validações encapsuladas nos Value Objects
    // Código mais limpo e expressivo
}

final readonly class Email
{
    public function __construct(public string $valor)
    {
        if (!$this->isEmailValido($valor)) {
            throw new InvalidArgumentException("Email inválido: {$valor}");
        }
    }
    
    public function equals(Email $outro): bool
    {
        return $this->valor === $outro->valor;
    }
    
    public function getDominio(): string
    {
        return substr($this->valor, strpos($this->valor, '@') + 1);
    }
    
    private function isEmailValido(string $email): bool
    {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
}

final readonly class Nome
{
    public function __construct(public string $valor)
    {
        if (empty(trim($valor))) {
            throw new InvalidArgumentException("Nome não pode estar vazio");
        }
        if (strlen($valor) < 2 || strlen($valor) > 100) {
            throw new InvalidArgumentException("Nome deve ter entre 2 e 100 caracteres");
        }
    }
    
    public function getPrimeiroNome(): string
    {
        return explode(' ', $this->valor)[0];
    }
    
    public function getIniciais(): string
    {
        $nomes = explode(' ', $this->valor);
        return implode('', array_map(fn($nome) => strtoupper($nome[0]), $nomes));
    }
}

Implementação de Value Objects: Boas Práticas

1. Sempre Imutáveis

final readonly class Coordenada
{
    public function __construct(
        public float $latitude,
        public float $longitude
    ) {
        if ($latitude < -90 || $latitude > 90) {
            throw new InvalidArgumentException("Latitude inválida: deve estar entre -90 e 90");
        }
        if ($longitude < -180 || $longitude > 180) {
            throw new InvalidArgumentException("Longitude inválida: deve estar entre -180 e 180");
        }
    }
    
    // Operações retornam novas instâncias
    public function mover(float $deltaLat, float $deltaLon): self
    {
        return new self($this->latitude + $deltaLat, $this->longitude + $deltaLon);
    }
    
    public function distanciaAte(Coordenada $outra): float
    {
        $deltaLat = deg2rad($outra->latitude - $this->latitude);
        $deltaLon = deg2rad($outra->longitude - $this->longitude);
        
        $a = sin($deltaLat / 2) * sin($deltaLat / 2) +
             cos(deg2rad($this->latitude)) * cos(deg2rad($outra->latitude)) *
             sin($deltaLon / 2) * sin($deltaLon / 2);
        
        $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
        
        return 6371 * $c; // Raio da Terra em km
    }
    
    public function equals(Coordenada $outra): bool
    {
        return abs($this->latitude - $outra->latitude) < 0.0001
            && abs($this->longitude - $outra->longitude) < 0.0001;
    }
}

2. Validação no Construtor

final readonly class Cpf
{
    public function __construct(public string $numero)
    {
        $cpfLimpo = $this->limparCpf($numero);
        if (!$this->isCpfValido($cpfLimpo)) {
            throw new InvalidArgumentException("CPF inválido: {$numero}");
        }
        $this->numero = $cpfLimpo;
    }
    
    public function formatado(): string
    {
        return substr($this->numero, 0, 3) . '.' .
               substr($this->numero, 3, 3) . '.' .
               substr($this->numero, 6, 3) . '-' .
               substr($this->numero, 9, 2);
    }
    
    public function equals(Cpf $outro): bool
    {
        return $this->numero === $outro->numero;
    }
    
    private function limparCpf(string $cpf): string
    {
        return preg_replace('/[^0-9]/', '', $cpf);
    }
    
    private function isCpfValido(string $cpf): bool
    {
        if (strlen($cpf) !== 11) {
            return false;
        }
        
        // Verifica se todos os dígitos são iguais
        if (preg_match('/^(\d)\1+$/', $cpf)) {
            return false;
        }
        
        return $this->calcularDigitoVerificador($cpf);
    }
    
    private function calcularDigitoVerificador(string $cpf): bool
    {
        // Primeiro dígito verificador
        $soma = 0;
        for ($i = 0; $i < 9; $i++) {
            $soma += intval($cpf[$i]) * (10 - $i);
        }
        $digito1 = $soma % 11 < 2 ? 0 : 11 - ($soma % 11);
        
        // Segundo dígito verificador
        $soma = 0;
        for ($i = 0; $i < 10; $i++) {
            $soma += intval($cpf[$i]) * (11 - $i);
        }
        $digito2 = $soma % 11 < 2 ? 0 : 11 - ($soma % 11);
        
        return intval($cpf[9]) === $digito1 && intval($cpf[10]) === $digito2;
    }
}

Entities: Gerenciando Identidade e Estado

Implementação de Identidade

class Produto
{
    private int $estoque;
    
    public function __construct(
        private readonly ProdutoId $id,
        private string $nome,
        private Dinheiro $preco,
        private CategoriaProduto $categoria
    ) {
        if (empty(trim($nome))) {
            throw new InvalidArgumentException("Nome é obrigatório");
        }
        $this->estoque = 0;
    }
    
    // Igualdade baseada apenas na identidade
    public function equals(Produto $outro): bool
    {
        return $this->id->equals($outro->id);
    }
    
    // Métodos de negócio
    public function ajustarPreco(Dinheiro $novoPreco): void
    {
        if ($novoPreco->valor < 0) {
            throw new InvalidArgumentException("Preço não pode ser negativo");
        }
        $this->preco = $novoPreco;
    }
    
    public function reporEstoque(int $quantidade): void
    {
        if ($quantidade <= 0) {
            throw new InvalidArgumentException("Quantidade deve ser positiva");
        }
        $this->estoque += $quantidade;
    }
    
    public function reduzirEstoque(int $quantidade): void
    {
        if (!$this->podeVender($quantidade)) {
            throw new InvalidArgumentException("Estoque insuficiente");
        }
        $this->estoque -= $quantidade;
    }
    
    public function podeVender(int $quantidade): bool
    {
        return $this->estoque >= $quantidade;
    }
    
    public function isElegivelParaPromocao(): bool
    {
        return $this->preco->isMaiorQue(Dinheiro::reais(100));
    }
    
    public function aplicarDesconto(float $percentual): void
    {
        if ($percentual < 0 || $percentual > 1) {
            throw new InvalidArgumentException("Percentual deve estar entre 0 e 1");
        }
        
        $novoValor = $this->preco->valor * (1 - $percentual);
        $this->preco = new Dinheiro($novoValor, $this->preco->moeda);
    }
    
    public function getId(): ProdutoId
    {
        return $this->id;
    }
    
    public function getNome(): string
    {
        return $this->nome;
    }
    
    public function getPreco(): Dinheiro
    {
        return $this->preco;
    }
    
    public function getEstoque(): int
    {
        return $this->estoque;
    }
}

Value Objects como Identificadores

final readonly class ProdutoId
{
    public function __construct(public string $valor)
    {
        if (empty(trim($valor))) {
            throw new InvalidArgumentException("ID do produto não pode ser vazio");
        }
    }
    
    public static function gerar(): self
    {
        return new self("PROD-" . strtoupper(substr(uniqid(), -8)));
    }
    
    public static function de(string $valor): self
    {
        return new self($valor);
    }
    
    public function equals(ProdutoId $outro): bool
    {
        return $this->valor === $outro->valor;
    }
    
    public function __toString(): string
    {
        return $this->valor;
    }
}

enum CategoriaProduto: string
{
    case ELETRONICOS = 'eletronicos';
    case ROUPAS = 'roupas';
    case LIVROS = 'livros';
    case CASA = 'casa';
    case ESPORTES = 'esportes';
}

Erros Comuns e Como Evitá-los

1. Entities Anêmicas

Problema:

class Produto
{
    private string $id;
    private string $nome;
    private float $preco;
    
    // Apenas getters e setters
    public function getId(): string { return $this->id; }
    public function setId(string $id): void { $this->id = $id; }
    public function getNome(): string { return $this->nome; }
    public function setNome(string $nome): void { $this->nome = $nome; }
    public function getPreco(): float { return $this->preco; }
    public function setPreco(float $preco): void { $this->preco = $preco; }
}

Solução:

class Produto
{
    public function __construct(
        private readonly ProdutoId $id,
        private string $nome,
        private Dinheiro $preco
    ) {}
    
    // Comportamentos do domínio
    public function aplicarDesconto(float $percentual): void
    {
        if ($percentual < 0 || $percentual > 1) {
            throw new InvalidArgumentException("Percentual inválido");
        }
        
        $novoValor = $this->preco->valor * (1 - $percentual);
        $this->preco = new Dinheiro($novoValor, $this->preco->moeda);
    }
    
    public function isElegivelParaPromocao(): bool
    {
        return $this->preco->isMaiorQue(Dinheiro::reais(100));
    }
    
    public function podeSerVendido(): bool
    {
        return $this->preco->valor > 0;
    }
}

2. Value Objects Mutáveis

Problema:

class Endereco
{
    private string $rua;
    private string $cep;
    
    // Permite modificação - PROBLEMA!
    public function setRua(string $rua): void { 
        $this->rua = $rua; 
    }
    
    public function setCep(string $cep): void { 
        $this->cep = $cep; 
    }
}

Solução:

final readonly class Endereco
{
    public function __construct(
        public string $rua,
        public string $cep,
        public string $cidade,
        public string $estado
    ) {
        $this->validarCep($cep);
    }
    
    // Imutável - para mudar, crie uma nova instância
    public function comNovaRua(string $novaRua): self
    {
        return new self($novaRua, $this->cep, $this->cidade, $this->estado);
    }
    
    public function comNovoCep(string $novoCep): self
    {
        return new self($this->rua, $novoCep, $this->cidade, $this->estado);
    }
    
    private function validarCep(string $cep): void
    {
        if (!preg_match('/^\d{5}-?\d{3}$/', $cep)) {
            throw new InvalidArgumentException("CEP inválido: {$cep}");
        }
    }
}

3. Identificadores Primitivos

Problema:

function transferirProduto(string $produtoId, string $categoriaId): void
{
    // Fácil trocar os parâmetros por engano
    // Compilador não detecta o erro
}

// Chamada incorreta - erro silencioso
transferirProduto($categoriaId, $produtoId);

Solução:

function transferirProduto(ProdutoId $produtoId, CategoriaId $categoriaId): void
{
    // Tipos específicos impedem erros
}

// Erro de compilação se trocar os parâmetros
transferirProduto($categoriaId, $produtoId); // TypeError!

Exemplo de Uso Completo

Para demonstrar como Entities e Value Objects trabalham juntos na prática, vamos criar um exemplo completo de um sistema de e-commerce:

// Value Objects já implementados anteriormente...

// Entity
class Cliente
{
    public function __construct(
        private readonly ClienteId $id,
        private Nome $nome,
        private Email $email,
        private Endereco $endereco,
        private readonly DateTimeImmutable $dataCadastro = new DateTimeImmutable()
    ) {}
    
    public function atualizarEmail(Email $novoEmail): void
    {
        if ($novoEmail->equals($this->email)) {
            return; // Não há mudança
        }
        $this->email = $novoEmail;
        // Poderia disparar um evento de domínio aqui
    }
    
    public function mudarEndereco(Endereco $novoEndereco): void
    {
        $this->endereco = $novoEndereco;
    }
    
    public function atualizarNome(Nome $novoNome): void
    {
        $this->nome = $novoNome;
    }
    
    public function getIdadeEmDias(): int
    {
        return $this->dataCadastro->diff(new DateTimeImmutable())->days;
    }
    
    public function isClienteAntigo(): bool
    {
        return $this->getIdadeEmDias() > 365;
    }
    
    // Identidade baseada no ID
    public function equals(Cliente $outro): bool
    {
        return $this->id->equals($outro->id);
    }
    
    // Getters
    public function getId(): ClienteId { return $this->id; }
    public function getNome(): Nome { return $this->nome; }
    public function getEmail(): Email { return $this->email; }
    public function getEndereco(): Endereco { return $this->endereco; }
    public function getDataCadastro(): DateTimeImmutable { return $this->dataCadastro; }
}

final readonly class ClienteId
{
    public function __construct(public string $valor)
    {
        if (empty(trim($valor))) {
            throw new InvalidArgumentException("ID do cliente não pode ser vazio");
        }
    }
    
    public static function gerar(): self
    {
        return new self("CLI-" . strtoupper(substr(uniqid(), -8)));
    }
    
    public function equals(ClienteId $outro): bool
    {
        return $this->valor === $outro->valor;
    }
}

// Uso
class ClienteService
{
    public function exemploUso(): void
    {
        // Criando Value Objects
        $id = ClienteId::gerar();
        $nome = new Nome("João Silva");
        $email = new Email("joao@email.com");
        $endereco = new Endereco("Rua A", "123", "12345-678", "São Paulo", "SP");
        
        // Criando Entity
        $cliente = new Cliente($id, $nome, $email, $endereco);
        
        // Operações mantêm identidade
        $cliente->atualizarEmail(new Email("joao.silva@email.com"));
        $cliente->mudarEndereco($endereco->comNovaRua("Rua B"));
        
        // Cliente continua sendo o mesmo (mesma identidade)
        assert($cliente->getId()->equals($id));
        
        // Demonstrando comportamentos
        echo "Cliente antigo: " . ($cliente->isClienteAntigo() ? "Sim" : "Não") . "\n";
        echo "Iniciais: " . $cliente->getNome()->getIniciais() . "\n";
        echo "Domínio do email: " . $cliente->getEmail()->getDominio() . "\n";
    }
}

Este exemplo demonstra como:

  1. Value Objects (Nome, Email, Endereco) encapsulam validações e comportamentos específicos
  2. Entity (Cliente) mantém identidade através do tempo e coordena os Value Objects
  3. Identidade única permite rastrear o cliente mesmo quando seus dados mudam
  4. Comportamentos de domínio são implementados onde fazem mais sentido

Conclusão

Entities e Value Objects são os blocos fundamentais de qualquer modelo de domínio rico em DDD. A distinção entre eles - baseada na questão da identidade - é crucial para criar um modelo que reflita verdadeiramente as regras e conceitos do negócio.

Entities representam conceitos que têm identidade e ciclo de vida, devendo ser rastreados ao longo do tempo. Elas encapsulam comportamentos do domínio e mantêm sua identidade mesmo quando seus atributos mudam.

Value Objects representam conceitos descritivos sem identidade, sendo definidos pelos seus atributos. São imutáveis, combatém a obsessão por primitivos e tornam o código mais expressivo e seguro.

A combinação inteligente de Entities e Value Objects resulta em um modelo de domínio mais expressivo, com regras de negócio bem encapsuladas e código mais limpo e testável. Lembre-se: o contexto sempre importa na decisão entre Entity e Value Object.

No próximo artigo da série, exploraremos Agregados e Aggregate Roots, aprendendo como agrupar Entities e Value Objects relacionados para manter a consistência e integridade do modelo de domínio.


Referências

  • Evans, Eric. "Domain-Driven Design: Tackling Complexity in the Heart of Software". Addison-Wesley, 2003.
  • Fowler, Martin. "Value Objects vs Entities". martinfowler.com
  • Jovanovic, Milan. "Value Objects in .NET (DDD Fundamentals)". milanjovanovic.tech, 2023.
  • Wempe, Jannik. "Domain-Driven Design: Distinguish Entities, Value Objects". blog.jannikwempe.com, 2021.
  • Pfeffer, Zohar. "Why you desperately need Value Objects in your life". dev.to, 2022.
  • Stemmler, Khalil. "Value Objects - DDD w/ TypeScript". khalilstemmler.com, 2019.
  • Vernon, Vaughn. "Implementing Domain-Driven Design". Addison-Wesley, 2013.
  • Khoriakov, Vladimir. "Value Object: a better implementation". enterprisecraftsmanship.com, 2017.
  • Young, Greg. "CQRS Documents". goodlydocs.io
  • Nilsson, Jimmy. "Applying Domain-Driven Design and Patterns". Addison-Wesley, 2006.