Design orientado a domínio: eventos de domínio e eventos de integração em .Net | Arquitetura
-
Design orientado a domínio: eventos de domínio e eventos de integração em .Net | Arquitetura
-
Domain Driven Design: Domain Events and Integration Events in .Net
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:
- Criação de cesta.
- Adição de produtos.
- Finalização da cesta → cria pedido.
- Adição de endereço de entrega.
- Seleção de método de pagamento.
- Envio do pedido → gera fatura.
- 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
- Handlers rodam na mesma Unidade de Trabalho.
- 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