Skip to main content

Design orientado a domínio: eventos de domínio e eventos de integração em .Net | Arquitetura

Eventos de domínio no DDD — Guia com Exemplo Prático

Eventos de domínio são um elemento fundamental no DDD, mas certamente podem ser um campo minado para navegar.

💬 “Um Evento de Domínio captura a memória de algo interessante que afeta o domínio” — Martin Fowler

Eventos de domínio são usados para permitir que agregados reajam a uma mudança em outro agregado sem acoplá-los. Livros pioneiros sobre DDD, como Eric Evans e Vaughn Vernon, defendem o uso de mecanismos assíncronos para lidar com eventos de domínio.

No entanto, isso aumenta enormemente a complexidade dos aplicativos e o número de componentes que você deve hospedar, devido à proliferação de manipuladores de eventos.

Para lidar com essa complexidade, na maioria dos casos é aceitável — e muito mais simples — lidar com eventos de domínio dentro do mesmo processo e escopo de transação a partir dos quais foram publicados.

Para casos que exigem comportamento assíncrono e agregações em diferentes subdomínios ou microsserviços, podemos usar eventos de integração, publicados via Message Broker/Event Bus.

⚠️ É importante que os eventos de integração sejam publicados após a conclusão bem-sucedida de uma transação, com mecanismos que garantam que sejam sempre enviados — mais sobre isso mais adiante.


Exemplo de aplicação

O exemplo usa um aplicativo .NET para uma empresa fictícia de e-commerce chamada DDDMart.

  • O foco está na camada de domínio — sem APIs, para simplificar.
  • Aplicativo de console hospedando vários serviços em segundo plano.
  • Simulação de pedidos e pagamentos usando contextos limitados.

Subdomínios

O domínio é composto por três subdomínios:

  • Catálogo (Core) — catálogo de produtos.
  • Pedido (Core) — criação de cestas e pedidos.
  • Pagamentos (Suporte) — emissão e rastreamento de faturas, armazenamento de métodos de pagamento.

Cada subdomínio possui um ou mais contextos limitados, podendo ser hospedado como microsserviço.


Arquitetura

Foi utilizada Arquitetura Limpa e Arquitetura Centrada no Domínio para organizar o código.

Tecnologias usadas:

  • Banco de dados: SQLite via Entity Framework.
  • Event Bus: em memória (poderia ser RabbitMQ em produção).
  • Mediador: MediatR para eventos de domínio.
  • IoC: Autofac para injeção de dependência.

Fluxo do cenário simulado:

  1. Criação de cesta.
  2. Adição de produtos.
  3. Finalização da cesta → cria pedido.
  4. Adição de endereço de entrega.
  5. Seleção de método de pagamento.
  6. Envio do pedido → gera fatura.
  7. Pagamento da fatura → despacho do pedido.

Event Storming

Foi usado Event Storming para quebrar o domínio em subdomínios, agregados, contextos limitados e eventos.


Implementação de Eventos de Domínio

Os eventos de domínio devem ser gerados a partir de um agregado. É recomendável adiar o disparo até a conclusão do comando/operação no agregado.


Gerando um Evento de Domínio

Classe base AggregateRoot:

namespace DDDMart.SharedKernel
{
public abstract class AggregateRoot : Entity
{
private List<DomainEvent> _domainEvents;
public IReadOnlyCollection<DomainEvent> DomainEvents => _domainEvents?.AsReadOnly();

protected void AddDomainEvent(DomainEvent eventItem)
{
_domainEvents = _domainEvents ?? new List<DomainEvent>();
_domainEvents.Add(eventItem);
}

public void ClearDomainEvents()
{
_domainEvents?.Clear();
}
}
}

Exemplo em Order (subdomínio de pedidos):

public void Submit()
{
CheckIfCancelled();
if (Submitted)
throw new DomainException("Order has already been submitted");

if(Status != OrderStatus.PaymentMethodConfirmed)
throw new DomainException("Payment Method and Delivery Address must be selected before submitting the order");

Status = OrderStatus.Submitted;
AddDomainEvent(new OrderSubmittedDomainEvent(Id, CustomerId, PaymentMethodId, PaymentAddress, TotalPrice));
}

Despachando Eventos de Domínio

Usando EF Change Tracker antes de salvar no banco:

public async Task<bool> CommitAsync(CancellationToken cancellationToken = default)
{
await _mediator.DispatchEventsAsync(this);
await base.SaveChangesAsync(cancellationToken);
return true;
}

MediatR como mediador:

private static async Task DispatchDomainEventsAsync(this IMediator mediator, List<DomainEvent> domainEvents)
{
foreach (var domainEvent in domainEvents)
{
await mediator.Publish(domainEvent);
}
}

Consumindo Eventos de Domínio

Exemplo de BasketCheckedOutDomainEventHandler:

public override async Task HandleAsync(BasketCheckedOutDomainEvent @event)
{
var basket = await _basketsRepository.GetByIdAsync(@event.BasketId);
var order = Order.FromBasket(basket);
await _ordersRepository.InsertAsync(order);
}

Considerações

  1. Handlers rodam na mesma Unidade de Trabalho.
  2. A transação pai pode falhar no commit — evite ações irreversíveis dentro do handler (ex.: enviar e-mail, chamar APIs externas, publicar eventos de integração).

Implementação de Eventos de Integração

Eventos de integração devem ser publicados assíncronamente via Message Broker.


Problema

Se publicados logo após o commit, podem ser perdidos se falhar. Se publicados antes, podem ser enviados mesmo que a transação falhe.


Solução: Padrão Outbox

  • Eventos de integração são persistidos no banco como entidades.
  • Serviço separado lê e publica.
  • Exclusão apenas após confirmação de publicação.

Estrutura da entidade

public class OutboxIntegrationEvent : AggregateRoot
{
public OutboxIntegrationEvent(string eventName, string data)
{
EventName = eventName;
Data = data;
}
public string EventName { get; private set; }
public string Data { get; private set; }
}

Mapeamento

protected override IntegrationEvent MapDomainEvent<T>(T domainEvent)
{
return domainEvent switch
{
OrderSubmittedDomainEvent @event => new OrderSubmittedIntegrationEvent(...),
OrderCancelledDomainEvent @event => new OrderCancelledIntegrationEvent(...),
{ } => null
};
}

Publicação

Serviço IntegrationEventPublisher lê a tabela OutboxIntegrationEvents periodicamente e publica no Event Bus.

private async Task PublishOutboxEventsAsync()
{
var integrationEvents = context.OutboxIntegrationEvents.AsQueryable().ToList();

foreach (var integrationEvent in integrationEvents)
{
await PublishIntegrationEventAsync(integrationEvent, context);
}

context.OutboxIntegrationEvents.RemoveRange(integrationEvents);
await context.CommitAsync();
}

Pensamentos finais

  • Eventos de domínio e de integração são ferramentas poderosas para modelagem e desacoplamento.
  • O Outbox Pattern é essencial para confiabilidade em publicação assíncrona.
  • Em sistemas simples, parte dessas técnicas pode ser desnecessária — use conforme a necessidade.

📎 Código completo disponível em: GitHub - matt-bentley/DDDMart