DDD: CQRS e Event Sourcing - Separando comandos e consultas (Parte 12)

DDD14 min de leitura

Este é o décimo segundo artigo da série sobre Domain-Driven Design. Agora vamos explorar dois padrões avançados que trabalham excepcionalmente bem juntos: CQRS (Command Query Responsibility Segregation) e Event Sourcing. Estes padrões são fundamentais para construir sistemas altamente escaláveis, auditáveis e resilientes.

CQRS e Event Sourcing representam uma mudança de paradigma significativa na forma como pensamos sobre persistência e modelagem de dados. Enquanto a abordagem tradicional tenta usar o mesmo modelo para leitura e escrita, estes padrões reconhecem que essas operações têm necessidades fundamentalmente diferentes.

O que é CQRS?

CQRS (Command Query Responsibility Segregation) é um padrão arquitetural que separa completamente as operações de escrita (comandos) das operações de leitura (consultas) em modelos distintos. Martin Fowler define:

"CQRS significa separar a responsabilidade entre comandos (mudanças) e consultas (leituras) em diferentes modelos."

A Filosofia por Trás do CQRS

A separação entre comandos e consultas não é apenas uma questão técnica - é uma decisão arquitetural que reconhece que:

Comandos (escrita) se preocupam com:

  • Validação de regras de negócio
  • Consistência transacional
  • Integridade dos dados
  • Processamento sequencial

Consultas (leitura) se preocupam com:

  • Performance de acesso
  • Formatos específicos de apresentação
  • Agregação de dados
  • Escalabilidade horizontal

Esta separação permite otimizar cada lado independentemente, escolhendo as tecnologias e estratégias mais adequadas para cada necessidade específica.

Cenário Tradicional vs CQRS

Para entender melhor o valor do CQRS, vamos comparar uma implementação tradicional com uma implementação usando CQRS.

Abordagem Tradicional

Na abordagem tradicional, usamos o mesmo modelo e repositório para operações de leitura e escrita:

<?php

// ❌ PROBLEMA: Mesmo modelo para leitura e escrita
class ProdutoService
{
    private ProdutoRepository $produtoRepository;
    
    public function __construct(ProdutoRepository $produtoRepository)
    {
        $this->produtoRepository = $produtoRepository;
    }
    
    public function buscarProduto(string $id): ?ProdutoView
    {
        $produto = $this->produtoRepository->buscarPorId($id);
        
        if ($produto === null) {
            return null;
        }
        
        // Precisa transformar entidade rica em view simples
        return new ProdutoView(
            $produto->getId(),
            $produto->getNome(),
            $produto->getPreco()->getValor(),
            $produto->getDescricao(),
            $produto->getCategoria()->getNome(),
            $produto->getQuantidadeEstoque()
        );
    }
    
    public function atualizarPreco(string $id, float $novoPreco): void
    {
        $produto = $this->produtoRepository->buscarPorId($id);
        
        if ($produto === null) {
            throw new DomainException('Produto não encontrado');
        }
        
        $produto->alterarPreco(new Dinheiro($novoPreco));
        $this->produtoRepository->salvar($produto);
    }
}

Esta abordagem apresenta limitações:

  • A mesma entidade serve para casos de uso muito diferentes
  • Performance de leitura limitada pela complexidade do modelo de escrita
  • Dificuldade para otimizar consultas específicas
  • Acoplamento entre necessidades de leitura e escrita

Implementação com CQRS

Com CQRS, separamos completamente as responsabilidades:

<?php

// ✅ SOLUÇÃO: Separação clara entre comandos e consultas

// === LADO DOS COMANDOS (Write Side) ===

interface Command {}

class AlterarPrecoProdutoCommand implements Command
{
    public function __construct(
        public readonly string $produtoId,
        public readonly float $novoPreco
    ) {}
}

class AdicionarProdutoCommand implements Command
{
    public function __construct(
        public readonly string $nome,
        public readonly string $descricao,
        public readonly float $preco,
        public readonly string $categoriaId,
        public readonly int $quantidadeInicial
    ) {}
}

// Command Handlers - focados em regras de negócio
class ProdutoCommandHandler
{
    private ProdutoRepository $produtoRepository;
    private EventDispatcher $eventDispatcher;
    
    public function __construct(
        ProdutoRepository $produtoRepository,
        EventDispatcher $eventDispatcher
    ) {
        $this->produtoRepository = $produtoRepository;
        $this->eventDispatcher = $eventDispatcher;
    }
    
    public function handle(AlterarPrecoProdutoCommand $command): void
    {
        $produto = $this->produtoRepository->buscarPorId($command->produtoId);
        
        if ($produto === null) {
            throw new DomainException('Produto não encontrado');
        }
        
        $precoAnterior = $produto->getPreco()->getValor();
        $produto->alterarPreco(new Dinheiro($command->novoPreco));
        
        $this->produtoRepository->salvar($produto);
        
        // Publicar evento para atualizar side de leitura
        $this->eventDispatcher->publish(new ProdutoPrecoAlteradoEvent(
            $produto->getId(),
            $precoAnterior,
            $command->novoPreco,
            new DateTime()
        ));
    }
    
    public function handle(AdicionarProdutoCommand $command): void
    {
        $produto = Produto::criar(
            $command->nome,
            $command->descricao,
            new Dinheiro($command->preco),
            $command->categoriaId,
            $command->quantidadeInicial
        );
        
        $this->produtoRepository->salvar($produto);
        
        $this->eventDispatcher->publish(new ProdutoAdicionadoEvent(
            $produto->getId(),
            $produto->getNome(),
            $produto->getPreco()->getValor(),
            new DateTime()
        ));
    }
}

// === LADO DAS CONSULTAS (Read Side) ===

interface Query {}

class BuscarProdutoQuery implements Query
{
    public function __construct(
        public readonly string $produtoId
    ) {}
}

class ListarProdutosPorCategoriaQuery implements Query
{
    public function __construct(
        public readonly string $categoriaId,
        public readonly int $limite = 20,
        public readonly int $offset = 0
    ) {}
}

// Query Handlers - focados em performance de leitura
class ProdutoQueryHandler
{
    private ProdutoReadRepository $produtoReadRepository;
    
    public function __construct(ProdutoReadRepository $produtoReadRepository)
    {
        $this->produtoReadRepository = $produtoReadRepository;
    }
    
    public function handle(BuscarProdutoQuery $query): ?ProdutoView
    {
        return $this->produtoReadRepository->buscarPorId($query->produtoId);
    }
    
    public function handle(ListarProdutosPorCategoriaQuery $query): array
    {
        return $this->produtoReadRepository->listarPorCategoria(
            $query->categoriaId,
            $query->limite,
            $query->offset
        );
    }
}

// Views específicas para leitura - otimizadas para apresentação
class ProdutoView
{
    public function __construct(
        public readonly string $id,
        public readonly string $nome,
        public readonly float $preco,
        public readonly string $descricao,
        public readonly string $categoriaNome,
        public readonly int $quantidadeEstoque,
        public readonly bool $disponivel,
        public readonly DateTime $ultimaAtualizacao
    ) {}
}

class ProdutoListaView
{
    public function __construct(
        public readonly string $id,
        public readonly string $nome,
        public readonly float $preco,
        public readonly string $categoriaNome,
        public readonly bool $disponivel
    ) {}
}

O que é Event Sourcing?

Event Sourcing é um padrão onde o estado atual é derivado de uma sequência imutável de eventos, ao invés de armazenar apenas o estado atual. Em vez de fazer UPDATE em registros, armazenamos cada mudança como um evento.

Conceitos Fundamentais do Event Sourcing

Eventos como Fonte da Verdade: O estado atual é reconstruído pela reprodução (replay) de todos os eventos que levaram a ele.

Imutabilidade: Uma vez criados, os eventos nunca são alterados ou deletados.

Auditoria Completa: Temos um histórico completo de todas as mudanças que aconteceram no sistema.

Temporalidade: Podemos reconstruir o estado do sistema em qualquer ponto no tempo.

Comparação: Estado vs Eventos

Vamos ver a diferença prática:

<?php

// ❌ Abordagem tradicional: armazenar apenas estado atual
// Tabela: contas_bancarias
// id | titular_id | saldo | created_at | updated_at
// 1  | 123       | 1500  | 2024-01-01 | 2024-01-25

$conta = new ContaBancaria('1', '123', 1500.00);

// ✅ Event Sourcing: armazenar eventos que levaram ao estado
$eventos = [
    new ContaCriadaEvent('1', '123', 1000.00, new DateTime('2024-01-01')),
    new DepositoRealizadoEvent('1', 500.00, 'TED', new DateTime('2024-01-15')),
    new SaqueRealizadoEvent('1', 200.00, 'ATM', new DateTime('2024-01-20')),
    new DepositoRealizadoEvent('1', 200.00, 'PIX', new DateTime('2024-01-25'))
];

// Estado atual = resultado da aplicação de todos os eventos
// Saldo = 1000 + 500 - 200 + 200 = 1500

Implementando Event Sourcing

1. Definindo Eventos de Domínio

Primeiro, vamos criar a estrutura básica para eventos de domínio:

<?php

interface DomainEvent
{
    public function getEventId(): string;
    public function getAggregateId(): string;
    public function getEventType(): string;
    public function getTimestamp(): DateTime;
    public function getVersion(): int;
    public function getPayload(): array;
}

abstract class BaseDomainEvent implements DomainEvent
{
    protected string $eventId;
    protected string $aggregateId;
    protected DateTime $timestamp;
    protected int $version;
    
    public function __construct(
        string $aggregateId,
        int $version,
        ?DateTime $timestamp = null
    ) {
        $this->eventId = uniqid('evt_');
        $this->aggregateId = $aggregateId;
        $this->version = $version;
        $this->timestamp = $timestamp ?? new DateTime();
    }
    
    public function getEventId(): string
    {
        return $this->eventId;
    }
    
    public function getAggregateId(): string
    {
        return $this->aggregateId;
    }
    
    public function getTimestamp(): DateTime
    {
        return $this->timestamp;
    }
    
    public function getVersion(): int
    {
        return $this->version;
    }
    
    abstract public function getEventType(): string;
    abstract public function getPayload(): array;
}

// Eventos específicos do domínio
class ContaCriadaEvent extends BaseDomainEvent
{
    public function __construct(
        string $contaId,
        int $version,
        public readonly string $titularId,
        public readonly float $saldoInicial,
        ?DateTime $timestamp = null
    ) {
        parent::__construct($contaId, $version, $timestamp);
    }
    
    public function getEventType(): string
    {
        return 'ContaCriada';
    }
    
    public function getPayload(): array
    {
        return [
            'titularId' => $this->titularId,
            'saldoInicial' => $this->saldoInicial
        ];
    }
}

class DepositoRealizadoEvent extends BaseDomainEvent
{
    public function __construct(
        string $contaId,
        int $version,
        public readonly float $valor,
        public readonly string $origem,
        ?DateTime $timestamp = null
    ) {
        parent::__construct($contaId, $version, $timestamp);
    }
    
    public function getEventType(): string
    {
        return 'DepositoRealizado';
    }
    
    public function getPayload(): array
    {
        return [
            'valor' => $this->valor,
            'origem' => $this->origem
        ];
    }
}

class SaqueRealizadoEvent extends BaseDomainEvent
{
    public function __construct(
        string $contaId,
        int $version,
        public readonly float $valor,
        public readonly string $local,
        ?DateTime $timestamp = null
    ) {
        parent::__construct($contaId, $version, $timestamp);
    }
    
    public function getEventType(): string
    {
        return 'SaqueRealizado';
    }
    
    public function getPayload(): array
    {
        return [
            'valor' => $this->valor,
            'local' => $this->local
        ];
    }
}

2. Aggregate com Event Sourcing

Agora vamos implementar um aggregate que gera e consome eventos:

<?php

abstract class EventSourcedAggregate
{
    protected array $eventosNaoCommitados = [];
    protected int $versao = 0;
    
    protected function adicionarEvento(DomainEvent $evento): void
    {
        $this->eventosNaoCommitados[] = $evento;
        $this->aplicarEvento($evento);
        $this->versao++;
    }
    
    abstract protected function aplicarEvento(DomainEvent $evento): void;
    
    public function getEventosNaoCommitados(): array
    {
        return $this->eventosNaoCommitados;
    }
    
    public function marcarEventosComoCommitados(): void
    {
        $this->eventosNaoCommitados = [];
    }
    
    public function getVersao(): int
    {
        return $this->versao;
    }
    
    public static function fromHistory(array $eventos): static
    {
        $aggregate = new static();
        
        foreach ($eventos as $evento) {
            $aggregate->aplicarEvento($evento);
            $aggregate->versao = $evento->getVersion();
        }
        
        return $aggregate;
    }
}

class ContaBancaria extends EventSourcedAggregate
{
    private string $id = '';
    private string $titularId = '';
    private float $saldo = 0.0;
    private bool $ativa = false;
    
    public static function criar(
        string $id,
        string $titularId,
        float $saldoInicial
    ): self {
        $conta = new self();
        
        if ($saldoInicial < 0) {
            throw new DomainException('Saldo inicial não pode ser negativo');
        }
        
        $evento = new ContaCriadaEvent(
            $id,
            1,
            $titularId,
            $saldoInicial
        );
        
        $conta->adicionarEvento($evento);
        return $conta;
    }
    
    public function depositar(float $valor, string $origem): void
    {
        if (!$this->ativa) {
            throw new DomainException('Conta não está ativa');
        }
        
        if ($valor <= 0) {
            throw new DomainException('Valor do depósito deve ser positivo');
        }
        
        $evento = new DepositoRealizadoEvent(
            $this->id,
            $this->versao + 1,
            $valor,
            $origem
        );
        
        $this->adicionarEvento($evento);
    }
    
    public function sacar(float $valor, string $local): void
    {
        if (!$this->ativa) {
            throw new DomainException('Conta não está ativa');
        }
        
        if ($valor <= 0) {
            throw new DomainException('Valor do saque deve ser positivo');
        }
        
        if ($valor > $this->saldo) {
            throw new DomainException('Saldo insuficiente');
        }
        
        $evento = new SaqueRealizadoEvent(
            $this->id,
            $this->versao + 1,
            $valor,
            $local
        );
        
        $this->adicionarEvento($evento);
    }
    
    protected function aplicarEvento(DomainEvent $evento): void
    {
        switch ($evento->getEventType()) {
            case 'ContaCriada':
                $this->aplicarContaCriada($evento);
                break;
            case 'DepositoRealizado':
                $this->aplicarDepositoRealizado($evento);
                break;
            case 'SaqueRealizado':
                $this->aplicarSaqueRealizado($evento);
                break;
            default:
                throw new InvalidArgumentException('Tipo de evento desconhecido: ' . $evento->getEventType());
        }
    }
    
    private function aplicarContaCriada(ContaCriadaEvent $evento): void
    {
        $this->id = $evento->getAggregateId();
        $this->titularId = $evento->titularId;
        $this->saldo = $evento->saldoInicial;
        $this->ativa = true;
    }
    
    private function aplicarDepositoRealizado(DepositoRealizadoEvent $evento): void
    {
        $this->saldo += $evento->valor;
    }
    
    private function aplicarSaqueRealizado(SaqueRealizadoEvent $evento): void
    {
        $this->saldo -= $evento->valor;
    }
    
    // Getters
    public function getId(): string
    {
        return $this->id;
    }
    
    public function getSaldo(): float
    {
        return $this->saldo;
    }
    
    public function isAtiva(): bool
    {
        return $this->ativa;
    }
}

3. Event Store - Armazenando Eventos

O Event Store é responsável por persistir e recuperar eventos:

<?php

interface EventStore
{
    public function salvarEventos(string $aggregateId, array $eventos, int $versaoEsperada): void;
    public function buscarEventos(string $aggregateId): array;
    public function buscarEventosApartirVersao(string $aggregateId, int $versao): array;
}

class MySQLEventStore implements EventStore
{
    private PDO $pdo;
    
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }
    
    public function salvarEventos(string $aggregateId, array $eventos, int $versaoEsperada): void
    {
        $this->pdo->beginTransaction();
        
        try {
            // Verificar versão para evitar conflitos
            $stmt = $this->pdo->prepare(
                'SELECT MAX(version) as max_version FROM events WHERE aggregate_id = ?'
            );
            $stmt->execute([$aggregateId]);
            $result = $stmt->fetch(PDO::FETCH_ASSOC);
            
            $versaoAtual = $result['max_version'] ?? 0;
            
            if ($versaoAtual !== $versaoEsperada) {
                throw new DomainException('Conflito de concorrência detectado');
            }
            
            // Salvar novos eventos
            $stmt = $this->pdo->prepare(
                'INSERT INTO events (event_id, aggregate_id, event_type, version, payload, timestamp) 
                 VALUES (?, ?, ?, ?, ?, ?)'
            );
            
            foreach ($eventos as $evento) {
                $stmt->execute([
                    $evento->getEventId(),
                    $evento->getAggregateId(),
                    $evento->getEventType(),
                    $evento->getVersion(),
                    json_encode($evento->getPayload()),
                    $evento->getTimestamp()->format('Y-m-d H:i:s')
                ]);
            }
            
            $this->pdo->commit();
        } catch (Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }
    
    public function buscarEventos(string $aggregateId): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM events WHERE aggregate_id = ? ORDER BY version ASC'
        );
        $stmt->execute([$aggregateId]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        return array_map([$this, 'reconstruirEvento'], $rows);
    }
    
    public function buscarEventosApartirVersao(string $aggregateId, int $versao): array
    {
        $stmt = $this->pdo->prepare(
            'SELECT * FROM events WHERE aggregate_id = ? AND version > ? ORDER BY version ASC'
        );
        $stmt->execute([$aggregateId, $versao]);
        $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
        
        return array_map([$this, 'reconstruirEvento'], $rows);
    }
    
    private function reconstruirEvento(array $row): DomainEvent
    {
        $payload = json_decode($row['payload'], true);
        $timestamp = new DateTime($row['timestamp']);
        
        switch ($row['event_type']) {
            case 'ContaCriada':
                return new ContaCriadaEvent(
                    $row['aggregate_id'],
                    (int)$row['version'],
                    $payload['titularId'],
                    (float)$payload['saldoInicial'],
                    $timestamp
                );
            case 'DepositoRealizado':
                return new DepositoRealizadoEvent(
                    $row['aggregate_id'],
                    (int)$row['version'],
                    (float)$payload['valor'],
                    $payload['origem'],
                    $timestamp
                );
            case 'SaqueRealizado':
                return new SaqueRealizadoEvent(
                    $row['aggregate_id'],
                    (int)$row['version'],
                    (float)$payload['valor'],
                    $payload['local'],
                    $timestamp
                );
            default:
                throw new InvalidArgumentException('Tipo de evento desconhecido: ' . $row['event_type']);
        }
    }
}

4. Repository com Event Sourcing

O repository trabalha com o Event Store para reconstituir aggregates:

<?php

class ContaBancariaRepository
{
    private EventStore $eventStore;
    
    public function __construct(EventStore $eventStore)
    {
        $this->eventStore = $eventStore;
    }
    
    public function buscarPorId(string $id): ?ContaBancaria
    {
        $eventos = $this->eventStore->buscarEventos($id);
        
        if (empty($eventos)) {
            return null;
        }
        
        return ContaBancaria::fromHistory($eventos);
    }
    
    public function salvar(ContaBancaria $conta): void
    {
        $eventosNaoCommitados = $conta->getEventosNaoCommitados();
        
        if (empty($eventosNaoCommitados)) {
            return;
        }
        
        $versaoEsperada = $conta->getVersao() - count($eventosNaoCommitados);
        
        $this->eventStore->salvarEventos(
            $conta->getId(),
            $eventosNaoCommitados,
            $versaoEsperada
        );
        
        $conta->marcarEventosComoCommitados();
    }
}

Projeções e Read Models

Com Event Sourcing, criamos projeções otimizadas para consultas específicas:

<?php

// Projeção para saldo atual
class SaldoAtualProjection
{
    private PDO $pdo;
    
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }
    
    public function handle(DomainEvent $evento): void
    {
        switch ($evento->getEventType()) {
            case 'ContaCriada':
                $this->criarRegistroSaldo($evento);
                break;
            case 'DepositoRealizado':
                $this->adicionarValor($evento);
                break;
            case 'SaqueRealizado':
                $this->subtrairValor($evento);
                break;
        }
    }
    
    private function criarRegistroSaldo(ContaCriadaEvent $evento): void
    {
        $stmt = $this->pdo->prepare(
            'INSERT INTO saldos_atuais (conta_id, saldo, ultima_atualizacao) VALUES (?, ?, ?)'
        );
        $stmt->execute([
            $evento->getAggregateId(),
            $evento->saldoInicial,
            $evento->getTimestamp()->format('Y-m-d H:i:s')
        ]);
    }
    
    private function adicionarValor(DepositoRealizadoEvent $evento): void
    {
        $stmt = $this->pdo->prepare(
            'UPDATE saldos_atuais SET saldo = saldo + ?, ultima_atualizacao = ? WHERE conta_id = ?'
        );
        $stmt->execute([
            $evento->valor,
            $evento->getTimestamp()->format('Y-m-d H:i:s'),
            $evento->getAggregateId()
        ]);
    }
    
    private function subtrairValor(SaqueRealizadoEvent $evento): void
    {
        $stmt = $this->pdo->prepare(
            'UPDATE saldos_atuais SET saldo = saldo - ?, ultima_atualizacao = ? WHERE conta_id = ?'
        );
        $stmt->execute([
            $evento->valor,
            $evento->getTimestamp()->format('Y-m-d H:i:s'),
            $evento->getAggregateId()
        ]);
    }
}

// Projeção para extrato
class ExtratoProjection
{
    private PDO $pdo;
    
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }
    
    public function handle(DomainEvent $evento): void
    {
        switch ($evento->getEventType()) {
            case 'DepositoRealizado':
                $this->adicionarMovimentacao($evento, 'CREDITO');
                break;
            case 'SaqueRealizado':
                $this->adicionarMovimentacao($evento, 'DEBITO');
                break;
        }
    }
    
    private function adicionarMovimentacao(DomainEvent $evento, string $tipo): void
    {
        $payload = $evento->getPayload();
        
        $stmt = $this->pdo->prepare(
            'INSERT INTO extrato (conta_id, tipo, valor, descricao, data_movimentacao) VALUES (?, ?, ?, ?, ?)'
        );
        $stmt->execute([
            $evento->getAggregateId(),
            $tipo,
            $payload['valor'],
            $tipo === 'CREDITO' ? 'Depósito via ' . $payload['origem'] : 'Saque em ' . $payload['local'],
            $evento->getTimestamp()->format('Y-m-d H:i:s')
        ]);
    }
}

Vantagens e Desvantagens

Vantagens do CQRS + Event Sourcing

Escalabilidade: Reads e writes podem ser otimizados e escalados independentemente.

Auditoria Completa: Histórico completo de todas as mudanças no sistema.

Flexibilidade: Novas projeções podem ser criadas sem afetar o modelo de escrita.

Resiliência: Eventos imutáveis facilitam a recuperação de falhas.

Insights de Negócio: O histórico de eventos oferece dados valiosos para análise.

Desvantagens e Cuidados

Complexidade: Significativamente mais complexo que uma abordagem tradicional.

Consistência Eventual: Reads podem estar temporariamente defasados.

Curva de Aprendizado: Requer mudança de mindset da equipe.

Overhead: Pode ser excessivo para sistemas simples.

Debugging: Mais difícil debuggar problemas em produção.

Quando Usar CQRS e Event Sourcing

Cenários Ideais

Sistemas com Alta Escala: Quando reads e writes têm padrões de acesso muito diferentes.

Auditoria Rigorosa: Sistemas financeiros, médicos, legais que precisam de histórico completo.

Análise de Comportamento: Quando você precisa entender "como" os dados mudaram, não apenas "o que" mudou.

Múltiplas Representações: Quando os mesmos dados precisam ser apresentados de formas muito diferentes.

Quando NÃO Usar

Sistemas Simples: CRUD básico sem necessidades especiais de performance ou auditoria.

Equipes Inexperientes: O padrão requer conhecimento sólido de DDD e arquitetura.

Requisitos de Consistência Imediata: Quando não pode haver delay entre write e read.

Conclusão

CQRS e Event Sourcing são padrões poderosos que, quando aplicados corretamente, podem resolver problemas complexos de escalabilidade, auditoria e flexibilidade. No entanto, eles vêm com complexidade adicional significativa e devem ser usados apenas quando os benefícios justificam claramente essa complexidade.

A implementação destes padrões requer planejamento cuidadoso, especialmente na definição de eventos e projeções. Mas quando bem implementados, eles oferecem uma base sólida para sistemas altamente escaláveis e auditáveis.

No próximo e último artigo da série, exploraremos Hexagonal Architecture e Clean Architecture, padrões arquiteturais que complementam perfeitamente os conceitos de DDD que estudamos.

Referências