DEV Community

Cover image for Event Sourcing Parte 4: Domain Events
William Santos
William Santos

Posted on

Event Sourcing Parte 4: Domain Events

Olá!

Chegamos à quarta parte de nossa série sobre Event Sourcing (ES) e, a partir de agora, falaremos sobre como este padrão interage com outros e quais as consequências dessas interações para as nossas aplicações.

Neste artigo falaremos sobre Eventos de Domínio (Domain Events), que servirá de ponte para um padrão que, segundo Greg Young, propositor do ES, é indispensável quando ES é utilizado: CQRS.

Vamos lá!

Eventos? Sim! Iguais? Não!

É a partir deste ponto que a complexidade do ES se faz notar mais explicitamente.

Recuperando o que discutimos em artigos anteriores, eventos do ES representam uma mudança de estado ocorrida em um dado modelo de domínio, tendo relevância apenas em seu escopo. Ou seja, os eventos do ES só dizem respeito ao modelo que os originou.

Os eventos de domínio, por sua vez, tem como escopo todo o domínio, ou seja, não afeta o modelo que o originou, mas a outros modelos de domínio que compõem a aplicação.
Isso significa que um evento de domínio descreve uma mudança no estado do próprio processo de negócio.

Confuso? Vamos a um exemplo hipotético.

Imagine um cenário onde temos uma loja virtual simples, cujo único processo de negócio seja o da venda de produtos.
Imagine que surja o requisito de auditarmos os carrinhos de compras e que, a partir de agora, cada inserção ou remoção de um produto do carrinho, ou qualquer alteração em sua quantidade, precise ser registrada.
Como vimos nos artigos anteriores, basta criarmos eventos como ProductAdded ou ProductQuantityUpdated e teremos nossos registros de auditoria.
Agora, responda à seguinte questão: para além da auditoria, qual a relevância da manipulação de um carrinho de compras para o processo de venda de produtos? Pois é! Nenhuma! Do ponto de vista do processo, o carrinho só tem relevância quando é submetido, porque é a partir de sua submissão que outras etapas do processo serão iniciadas, como o pagamento do pedido e a separação dos produtos no estoque.

Agora, como eu notifico o domínio de que um carrinho foi submetido se seus eventos se limitam a indicar suas mudanças de estado? É aí que entram em cena os eventos de domínio!

Neste mesmo cenário, criaríamos um evento de domínio chamado CartSubmited que seria emitido para toda a aplicação, e que estaria disponível a qualquer outro modelo de domínio interessado.

Ainda soa confuso? Vamos esclarecer estes pontos apresentando seu mecanismo.

Os Três Mosqueteiros dos Eventos de Domínio

Agora que entendemos que eventos de domínio são eventos relevantes para o todo o domínio, e não para um dado modelo, entendemos também que, caso uma mudança de estado do modelo seja relevante para todo o domínio, teremos dois eventos! Um evento do modelo, que ficará restrito a seu escopo, e outro evento, este de domínio, que será distribuído aos demais modelos interessados.

Para distribuirmos um evento de domínio, precisamos de três componentes: o evento de domínio (Domain Event), que é a mensagem que será enviada; o disparador de eventos (Event Dispatcher), que se encarregará de enviar a mensagem aos interessados, e os manipuladores de eventos (Event Handlers), que tratarão este evento de domínio dando a ele um destino em um processo de negócio.

O fluxo é o seguinte: i) um dado modelo registra um evento de domínio; ii) o disparador é invocado e o evento de domínio é passado como parâmetro; iii) os manipuladores recebem esse evento do disparador e decidem como agir a partir dele.

Me mostre o código!

Para começar, falaremos sobre nosso protagonista: o Evento de Domínio.

O evento de domínio, tal como o evento do modelo, é apenas a representação de uma mensagem marcada com uma interface. Veja os exemplos abaixo.

namespace Lab.EventSourcing.DomainEvents
{
    public interface IDomainEvent { }

    ...

    public class BuyOrderCancelledDomainEvent : IDomainEvent
    {
        public Guid AccountId { get; private set; }
        public decimal Amount { get; private set; }

        public BuyOrderCancelledDomainEvent(Guid accountId, decimal amount) =>
            (AccountId, Amount) = (accountId, amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

Simples. Não?

Em seguida, temos nosso disparador de eventos:

namespace Lab.EventSourcing.DomainEvents
{
    public interface IDomainEventDispatcher
    {
        void RegisterHandler<TEvent>(IDomainEventHandler handler)
            where TEvent : IDomainEvent;

        void Dispatch(IEnumerable<IDomainEvent> domainEvents);
    }

    ...

    public static class DomainEventDispatcher : IDomainEventDispatcher
    {
        private readonly ConcurrentDictionary<Type, List<IDomainEventHandler>> _handlers = 
                new ConcurrentDictionary<Type, List<IDomainEventHandler>>();

        public void RegisterHandler<TEvent>(IDomainEventHandler handler)
            where TEvent : IDomainEvent
        {
            if (_handlers.ContainsKey(typeof(TEvent)) 
                && _handlers[typeof(TEvent)].Any(h => h.GetType() == handler.GetType()))
                    throw new ArgumentException($"Handler of type {handler.GetType()} already registered.", nameof(handler));

            _handlers.AddOrUpdate(typeof(TEvent), 
                                  new List<IDomainEventHandler> { handler }, 
                                  (type, list) => { list.Add(handler); return list; });
        }

        public void Dispatch(IEnumerable<IDomainEvent> domainEvents)
        {
            if (domainEvents is null)
                throw new ArgumentNullException("A domain events collection must be provided.", nameof(domainEvents));

            foreach(var domainEvent in domainEvents)
                foreach (var handler in _handlers[domainEvent.GetType()])
                    handler.Handle(domainEvent);
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Vamos analisar nosso disparador.

Ele possui apenas dois métodos RegisterHandler, Dispatch. O primeiro é responsável registrar um IDomainEventHandler, associando-o a um dado tipo de evento. Enquanto Dispatch se encarregará de entregar a cada IDomainEventHandler a instância do evento ao qual foi relacionado.

Por fim, temos nosso manipulador de eventos, uma interface com um único método Handle, que receberá como parâmetro uma instância de IDomainEvent, o evento que será manipulado.

namespace Lab.EventSourcing.DomainEvents
{
    public interface IDomainEventHandler
    {
        void Handle(IDomainEvent domainEvent);
    }

    ...

    public class BuyOrderCreatedHandler : IDomainEventHandler
    {
        private readonly EventStore _eventStore;

        public BuyOrderCreatedHandler(EventStore eventStore) =>
            _eventStore = eventStore;

        public void Handle(IDomainEvent domainEvent)
        {
            var order = domainEvent as BuyOrderCreatedDomainEvent;
            if (order is null)
                throw new ArgumentException($"Unsuported event type {domainEvent.GetType()}.");

            var account = Account.Load(_eventStore.GetById(order.AccountId));
            account.Debit(order.Amount);

            _eventStore.Commit(account);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Agora que temos toda a infraestrutura necessária para o disparo e manipulação de eventos de domínio, precisamos adequar a ela o nosso modelo e a infraestrutura preexistente.

namespace Lab.EventSourcing.Core
{
    public abstract class EventSourcingModel<T> where T : EventSourcingModel<T>
    {
        private readonly Queue<IEvent> _pendingEvents = new Queue<IEvent>();
        public IReadOnlyCollection<IEvent> PendingEvents { get => _pendingEvents; }

        private readonly Queue<IDomainEvent> _domainEvents = new Queue<IDomainEvent>();
        public IReadOnlyCollection<IDomainEvent> DomainEvents { get => _domainEvents; }

        ...

        public void Commit()
        {
            _pendingEvents.Clear();
            _domainEvents.Clear();
        }

        protected void AddDomainEvent(IDomainEvent domainEvent) =>
            _domainEvents.Enqueue(domainEvent);
    }
}
Enter fullscreen mode Exit fullscreen mode

Repare que tanto no caso dos eventos do modelo, quanto no dos eventos de domínio, há o mesmo mecanismo: o enfileiramento de eventos e sua oferta como uma ReadonlyCollection. Entretanto, ainda que sejam muito parecidos em termos sintáticos, são completamente diferentes em termos semânticos e, por este motivo, estão separados e possuem contratos distintos (IEvent e IDomainEvent).

Agora nosso repositório de eventos também precisa ser ajustado. Isso porque será a partir dele que os eventos serão disparados. Ele ficará assim:

namespace Lab.EventSourcing.Core
{
    public class EventStore
    {
        private readonly EventStoreDbContext _eventStoreContext;
        private readonly IDomainEventDispatcher _domainEventDispatcher;

        public static EventStore Create(IDomainEventDispatcher domainEventDispatcher) =>
            new EventStore(domainEventDispatcher);

        private EventStore(IDomainEventDispatcher domainEventDispatcher)
        {
            _eventStoreContext = new EventStoreDbContext(new DbContextOptionsBuilder<EventStoreDbContext>()
                                                                .UseInMemoryDatabase(databaseName: "EventStore")
                                                                .EnableSensitiveDataLogging()
                                                                .Options);
            _domainEventDispatcher = domainEventDispatcher;
        }

        public void Commit<TModel>(TModel model) where TModel : EventSourcingModel<TModel>
        {
            ...
           _eventStoreContext.SaveChanges();

            _domainEventDispatcher.Dispatch(model.DomainEvents);

            model.Commit();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

A partir de agora, sempre que nossos eventos de modelo forem persistidos, nossos eventos de domínio serão disparados. Pode parecer estranho incluir essa responsabilidade no repositório de eventos, mas este é um atalho para garantir que não seja necessário lembrar de disparar os eventos de domínio quando um dado método for acionado -- afinal, é difícil lembrar quais métodos vão adicionar eventos de domínio além dos eventos do modelo! -- e que os eventos sejam sempre disparados, afinal de contas, o modelo sempre será persistido quando seu estado for modificado.

Pondo o conhecimento em prática.

Apesar de aparentemente difícil, a diferenciação entre eventos de modelo e eventos de domínio é bastante simples: quando um evento for necessário para que uma nova etapa do processo de negócio seja iniciada, utilize eventos de domínio além dos eventos de modelo. Quando não for necessário, utilize apenas o evento do modelo.

E, com isso, temos condições de implementar uma aplicação cujos modelos interajam por meio de eventos de domínio. Você pode clonar este repositório do Github para ter acesso ao código completo deste artigo, incluindo um projeto de testes que simula o seguinte cenário: uma corretora de valores possui contas nas quais o dinheiro do cliente é movimentado em operações com produtos financeiros. Esta conta será criada com um saldo inicial e, toda vez que o cliente enviar uma ordem de compra de ações, sua conta deve ser debitada de acordo com o financeiro desta ordem. E toda vez que uma ordem de compra for cancelada, sua conta deve ser creditada de acordo com o financeiro desta ordem.

Ou seja, sempre que uma ordem de compra for lançada, criaremos um evento de domínio que será encaminhado pelo disparador a um manipulador, que recuperará a conta do cliente e efetuará o débito em sua conta. Da mesma forma, sempre que uma ordem de compra for cancelada, um evento de domínio será lançado para que a conta seja recuperada e creditada.

Por hoje é só, pessoal! Mas antes...

É possível que você tenha notado algo importante: apesar de útil para o que se propõe, auditoria, o ES tem um problema fundamental: consultar o estado de nossos modelos a um baixo custo computacional -- afinal de contas, sempre que precisamos consultar um modelo, processamos todo o seu histórico de eventos. E, então, pode surgir a pergunta: como atendemos à interface de usuário com um custo tão alto para consultar nossos modelos? A resposta é: CQRS.

No próximo artigo apresentaremos a forma mais simples deste padrão, trazendo uma aplicação de exemplo que nos permite consultar nossos modelos como faríamos caso utilizássemos a persistência de modelos anêmicos -- porque, de certa forma, é isso que faremos!

Gostou deste artigo? Me deixe saber pelos indicadores. Ficou com alguma dúvida? Me pergunte pelos comentários, ou qualquer um dos meus contatos, que respondo assim que possível.

Até a próxima!

Top comments (4)

Collapse
 
ljtfreitas profile image
Tiago de Freitas Lima

Olá, obrigado e parabéns pelo artigo :). Se me permite alguns comentários, talvez eles nem façam sentido por você ter comentado sobre esses pontos nos artigos anteriores. Se for o caso, minhas desculpas. Mas comentando: eu não entendi bem o que você quis diferenciar como "eventos de domínio" e "eventos de modelo". A palavra "modelo" costuma se referir ao "domínio"...ao menos na literatura e no código fonte também é um entendimento comum...de todo modo, entendi que você quis diferenciar eventos de domínio e os que "não são parte do domínio" de alguma maneira? Se foi o caso não entendi bem essa diferenciação nesse exemplo específico, porque da perspectiva do event sourcing essencialmente tudo o que você armazena no event store é relevante no domĩnio. Mas é fato que existem eventos de aplicação/infraestrutura/view/whatever, que não se referem ao domínio, mas não entendi se era desse tipo de evento que voce se refere.
A respeito do exemplo em específico sobre e-commerce, bem, eu devo discordar do exemplo de que um carrinho só gera alguma consequência quando é fechado (baseado em experiencia pessoal trabalhando anos com e-commerce). Posso estar errado, claro, mas trabalhei em e-commerces, por exemplo, que um produto adicionado/removido/atualizado no carrinho disparava eventos para parceiros de marketing. Um produto adicionado/removido/atualizado no carrinho disparava mudanças em outros sistemas que mantinham uma espécie de "conversa" entre o comprador e o vendedor (durante o carrinho aberto). Mudanças em um carrinho geravam eventos para sistemas analíticos para tentar entender o comportamento do usuario. Enfim, um carrinho é algo core no domínio de um e-commerce. Então, juntando com a duvida anterior, acho que o exemplo ficou um pouco confuso pra mim ao menos. De todo modo, obrigado pelo artigo!

Collapse
 
wsantosdev profile image
William Santos • Edited

Fala, Tiago. Tudo bom?

Vou te responder por tópico:
1) Sobre eventos do modelo e de domínio.

Sim! Falo sobre isso em artigos anteriores, mas sua dúvida não é menos pertinente por causa disso!

Uso o termo "modelo" porque corresponde a um modelo de domínio (martinfowler.com/eaaCatalog/domain...), que pode ser uma entidade ou um agregado caso apliquemos ao DDD. A ideia é ser abrangente na definição para não me prender ao DDD, já que ele não é um requisito para o uso do padrão ES, mas outros textos na internet dão a entender que seja.
Sendo assim, quando me refiro evento do modelo, trato de um evento restrito à auditoria deste modelo, e que não dispara um processo de negócio.

Já um evento de domínio é seu oposto: não diz respeito à auditoria do estado da entidade, mas é de interesse de outros modelos do domínio – entenda o termo "relevância" como "interesse", acho que a separação fica mais clara.

Talvez haja uma confusão entre a definição de modelo de domínio, que é um componente do domínio, e de domínio em si, que poderia ser entendido todo o escopo do problema de negócio a se solucionar.

Consegui me expressar melhor?

2) Sobre a loja virtual.
Quando falo de relevância para o negócio, falo sobre a capacidade de um evento de disparar um novo processo de negócio – e os eventos que constam da Event Store não estão nessa categoria, já que visam, apenas, auditar o estado de um dado modelo de domínio. Os eventos que disparam novos processos de negócio são os eventos de domínio, e estes não são persistidos na Event Store – e posso afirmar que, se estiverem, trata-se de um erro grave. A persistência de eventos de domínio não é necessária, mas ela pode ocorrer em outro repositório que não a Event Store – essa diferenciação é muito importante!

No caso da loja virtual, minha intenção era restringir o escopo ao principal processo de negócio que é a venda de produtos.

Ou seja, ainda que as alterações no estado do carrinho pudessem ter relevância para outros processos de negócio, as mesmas não a tem para o processo de venda – e aí preciso admitir que me expressei muito mal ao omitir detalhes do cenário que poderiam ter evitado essa confusão, vou inclusive editar o texto para deixar o cenário explícito.

Consegui ajudar? Vou melhorar o texto nos próximos dias e espero que o releia e me diga se os conceitos ficaram mais claros. O que acha? E aproveito para te convidar à leitura dos artigos anteriores. Entendo que, partindo do início, as definições sejam mais fluídas.

Muito obrigado pelo feedback e pelas observações. Gosto muito dessa interação porque de outro modo não consigo melhorar a qualidade dos meus textos.

Valeu! :D

Collapse
 
ljtfreitas profile image
Tiago de Freitas Lima

Oi @Willian, obrigado pela resposta. Hã, olhando em retrospectiva talvez eu devesse ter lido os artigos anteriores primeiro (kkk). Minhas desculpas e obrigado por responder. Sobre a sua diferenciação de eventos que disparam outros processos (e os que não) e eventos que são armazenados (e os que não), creio que entendi o seu ponto, embora pense se isso não criaria uma complicação de design no código 🤔, porque um evento representa uma mudança de estado; e os fluxos subsequentes do programa (disparar um novo processo, como você comentou) dependem dos acontecimentos/mudanças de estado das entidades no programa. E então se diferenciarmos eventos que disparam, digamos, "outras" mudanças e os que só servem para tracker o estado talvez o próprio design se tornasse confuso. Mas é interessante, já que o propósito do event sourcing é de fato tracker as mudanças de estado através de eventos e não disparar mais mudanças; isso é apenas algo que as pessoas decidiram fazer hehe. Então por outro lado ter esses dois conceitos separados como você demonstrou é interessante. Sobre o exemplo do carrinho, acho que só comentei porque o exemplo me pareceu estranho (baseado unicamente na minha experiência pessoal, é claro 😆), mas de todo modo está bem encaixado com o restante da explicação e não é um problema. Novamente obrigado! :)

Thread Thread
 
wsantosdev profile image
William Santos

Fala, Tiago! Não precisa agradecer, é um prazer conversar por aqui.

A diferenciação entre os eventos dos modelos e os eventos de domínio é algo que entendo fundamental, e por dois motivos:

1) Trazer clareza sobre o propósito de cada evento – e aí discordo que a separação adicione complexidade, porque na verdade evidencia a natureza de cada tipo de evento;
2) Evitar que eventos que não sejam relevantes para o domínio sejam disparados inutilmente – algo que geraria um tremendo desperdício de recursos, uma vez que a tendência é haver muito menos eventos de domínio que eventos do modelo.

Como são conceitos diferentes, precisam existir isoladamente.

Espero que acompanhe os próximos artigos, já estamos próximos do fim da série!

Abraço. :)