Design: Event Sourcing (5 anos depois)
Olá! Este é mais um post da seção Design e nele trago uma atualização da série sobre Event Sourcing, de 2020. Como o blog está comemorando 5 anos, entendi fazer sentido ilustrar o padrão com uma aplicação funcional. Este post foi inspirado pelo livro de Martin Dilger chamado Understanding EventSourcing (em inglês). Sua abordagem é bastante interessante e, além disso, inclui uma explicação completa sobre modelagem de domínio orientada a eventos, a Event Modeling, concebida por Adam Dymitruk. No repositório deste post estão diagramas de Event Modeling, que vou explorar no próximo post. Portanto não se preocupe caso sua leitura não seja óbvia logo de início. Antes de mais nada, muito obrigado! Antes de começarmos a explorar a aplicação, quero agradecer a você que esteve acompanhando este blog desde o início, 5 anos atrás. O apoio da comunidade é um excelente combustível para continuar a escrever (mesmo que com um hiato no meio do caminho) e cada curtida, comentário e compartilhamento ajuda muito. Então, muito obrigado! Sigamos. Event Sourcing Os módulos da aplicação de demonstração empregam este pattern para registrar o histórico de suas mudanças de estado enquanto, ao mesmo tempo, nos permite criar projeções (visões) nos mais diferentes moldes para uso analítico pelo negócio. Vamos ver o que o livro nos diz a esse respeito (em tradução livre): "É sobre armazenar informações e construir diferentes visões a partir dos dados na EventStore. (...) Eventos são simples fatos e de forma alguma técnicos. Sempre que algo acontece no sistema nós armazenamos como um Evento. Então, em termos simples, Eventos são registros do passado, portanto os escrevemos no passado. Em vez de 'cliente enviando um pedido' dizemos 'Um pedido foi enviado', ou encurtando, 'PedidoEnviado'. Um pedido foi enviado e não há como mudar esse fato. Mesmo excluindo o pedido de sua base de dados não mudará o fato de que um pedido foi enviado antes". Snapshots Como dito no post original sobre o tema, snapshots são uma forma útil de reduzir a carga sobre nosso Event Store, criando um ponto de restauração e demandando que apenas os eventos que lhes sejam posteriores sejam carregados, fazendo com que a obtenção de nosso modelo de escrita, nossa entidade, tenha um desempenho melhor. O livro sugere o mesmo em outras palavras (como sempre, em tradução livre): "Snapshots podem ser usados quando você deseja limitar o número de eventos que você precisa preocessar em uma stream. Visto por esse ângulo, snapshots servem a um propósito semelhante ao cache: nós adicionamos ao cache o estado projetado do sistema em vez de calculá-lo em tempo de execução." Projeções Como dito acima, Event Sourcing nos permite criar projeções à medida em que os eventos ocorrem. Podemos criá-las a partir de qualquer requisito de negócio que envolva os dados dos eventos, e no mesmo momento em que estes são persistidos. Ou seja, podemos criar entradas de relatórios em tempo real, escolhendo uma estratégia que pode variar de acordo com os requisitos do negócio. Ao receber os eventos do sistema, o responsável pela criação das projeções pode, por exemplo, iterar em uma lista de Projetores para em cada um, gerar um projeção analítica diferente. Quando houver a necessidade de mais projeções, um novo projetor pode ser adicionado. Em nossa aplicação há o módulo Accounts, que atende à conta de um dado cliente. Esta conta pode sofrer créditos e débitos ao longo de seu ciclo de vida e, graças ao emprego de Event Sourcing, podemos extrair dados que respondam a perguntas como "quantos depósitos foram feitos pelos clientes no mês de março versus o mês de fevereiro?", ou "qual o valor total em depósitos dos clientes ao longo dos últimos doze meses?". Responder a essas perguntas é importante para o negócio, dado que decisões estratégicas podem ser tomadas a partir delas. CQRS Como dito no post original sobre o assunto, CQRS nos fornece uma forma de persistir projeções que atendam às necessidades da nossa aplicação. No caso da conta do cliente mencionado acima, podemos criar um modelo tão simples quanto o identificador do cliente e seu saldo (como fazemos na aplicação de demonstração) para que seja exibido, por exemplo, na tela inicial de um app ou numa página de um sistema web. Vale lembrar que é possível criar visões diferentes a partir de um mesmo evento, ou seja, seria possível também criar uma entrada de extrato com o que foi depositado/retirado pelo cliente, facilitando assim a exibição dos dados. Consistência Algo muito importante ao se falar sobre CQRS é que muito se pensa que CQRS demanda, necessariamente, consistência eventual, e isso é um erro. Vamos recorrer ao livro mais uma vez para ilustrar esse caso (mais uma vez em tradução livre): "Para muitos projetos a solução mais simples para atualizar todas as partes do sistema de uma vez é usar uma mesma base de dados para os modelos de escrita e leitura, o que não é pro

Olá!
Este é mais um post da seção Design e nele trago uma atualização da série sobre Event Sourcing, de 2020. Como o blog está comemorando 5 anos, entendi fazer sentido ilustrar o padrão com uma aplicação funcional.
Este post foi inspirado pelo livro de Martin Dilger chamado Understanding EventSourcing (em inglês). Sua abordagem é bastante interessante e, além disso, inclui uma explicação completa sobre modelagem de domínio orientada a eventos, a Event Modeling, concebida por Adam Dymitruk.
No repositório deste post estão diagramas de Event Modeling, que vou explorar no próximo post. Portanto não se preocupe caso sua leitura não seja óbvia logo de início.
Antes de mais nada, muito obrigado!
Antes de começarmos a explorar a aplicação, quero agradecer a você que esteve acompanhando este blog desde o início, 5 anos atrás. O apoio da comunidade é um excelente combustível para continuar a escrever (mesmo que com um hiato no meio do caminho) e cada curtida, comentário e compartilhamento ajuda muito. Então, muito obrigado!
Sigamos.
Event Sourcing
Os módulos da aplicação de demonstração empregam este pattern para registrar o histórico de suas mudanças de estado enquanto, ao mesmo tempo, nos permite criar projeções (visões) nos mais diferentes moldes para uso analítico pelo negócio.
Vamos ver o que o livro nos diz a esse respeito (em tradução livre): "É sobre armazenar informações e construir diferentes visões a partir dos dados na EventStore. (...) Eventos são simples fatos e de forma alguma técnicos. Sempre que algo acontece no sistema nós armazenamos como um Evento. Então, em termos simples, Eventos são registros do passado, portanto os escrevemos no passado. Em vez de 'cliente enviando um pedido' dizemos 'Um pedido foi enviado', ou encurtando, 'PedidoEnviado'. Um pedido foi enviado e não há como mudar esse fato. Mesmo excluindo o pedido de sua base de dados não mudará o fato de que um pedido foi enviado antes".
Snapshots
Como dito no post original sobre o tema, snapshots são uma forma útil de reduzir a carga sobre nosso Event Store, criando um ponto de restauração e demandando que apenas os eventos que lhes sejam posteriores sejam carregados, fazendo com que a obtenção de nosso modelo de escrita, nossa entidade, tenha um desempenho melhor.
O livro sugere o mesmo em outras palavras (como sempre, em tradução livre): "Snapshots podem ser usados quando você deseja limitar o número de eventos que você precisa preocessar em uma stream. Visto por esse ângulo, snapshots servem a um propósito semelhante ao cache: nós adicionamos ao cache o estado projetado do sistema em vez de calculá-lo em tempo de execução."
Projeções
Como dito acima, Event Sourcing nos permite criar projeções à medida em que os eventos ocorrem. Podemos criá-las a partir de qualquer requisito de negócio que envolva os dados dos eventos, e no mesmo momento em que estes são persistidos.
Ou seja, podemos criar entradas de relatórios em tempo real, escolhendo uma estratégia que pode variar de acordo com os requisitos do negócio. Ao receber os eventos do sistema, o responsável pela criação das projeções pode, por exemplo, iterar em uma lista de Projetores para em cada um, gerar um projeção analítica diferente. Quando houver a necessidade de mais projeções, um novo projetor pode ser adicionado.
Em nossa aplicação há o módulo Accounts, que atende à conta de um dado cliente. Esta conta pode sofrer créditos e débitos ao longo de seu ciclo de vida e, graças ao emprego de Event Sourcing, podemos extrair dados que respondam a perguntas como "quantos depósitos foram feitos pelos clientes no mês de março versus o mês de fevereiro?", ou "qual o valor total em depósitos dos clientes ao longo dos últimos doze meses?".
Responder a essas perguntas é importante para o negócio, dado que decisões estratégicas podem ser tomadas a partir delas.
CQRS
Como dito no post original sobre o assunto, CQRS nos fornece uma forma de persistir projeções que atendam às necessidades da nossa aplicação. No caso da conta do cliente mencionado acima, podemos criar um modelo tão simples quanto o identificador do cliente e seu saldo (como fazemos na aplicação de demonstração) para que seja exibido, por exemplo, na tela inicial de um app ou numa página de um sistema web. Vale lembrar que é possível criar visões diferentes a partir de um mesmo evento, ou seja, seria possível também criar uma entrada de extrato com o que foi depositado/retirado pelo cliente, facilitando assim a exibição dos dados.
Consistência
Algo muito importante ao se falar sobre CQRS é que muito se pensa que CQRS demanda, necessariamente, consistência eventual, e isso é um erro. Vamos recorrer ao livro mais uma vez para ilustrar esse caso (mais uma vez em tradução livre): "Para muitos projetos a solução mais simples para atualizar todas as partes do sistema de uma vez é usar uma mesma base de dados para os modelos de escrita e leitura, o que não é proibido e nem uma má prática. Se você estiver utilizando um banco PostgreSQL para armazenar seus eventos assim como suas projeções é normalmente suficiente atualizá-los dentro de uma mesma transação. Essa abordagem garante consistência e simplifica muito o sistema, tornando fácil entender a respeito. Não subestime as vantagens da simplicidade. Eu me desviaria dela apenas se você tiver razões muito boas."
Em nossa aplicação de demonstração utilizei as duas formas de consistência, eventual para o módulo Account, e imediata, na mesma transação de armazenamento dos eventos, para os demais. Ao ler o código vai ficar clara a diferença entre ambas as abordagens, ainda que tenha simplificado muito a abordagem da consistência eventual para rodar no mesmo processo que a Web API.
Bom, falamos algumas vezes sobre a aplicação de demonstração. Certo? Hora de conhecê-la!
Conhecendo a aplicação
Esta aplicação é um monolito modular composto pelos seguintes módulos:
- Account (Como já mencionado, atende à conta do cliente e suas movimentações)
- Orders (Ordens de compra e venda de ações)
- Exchange (Um simulador do comportamento da bolsa de valores, que executa as ordens)
- Positions (A visualização das ações que o usuário possui)
Esses módulos são utilizados por uma Web API que expõe suas funcionalidades e compartilham a EventStore para registrar os eventos, assim como a Snapshot Store para os snapshots.
Introdução - Os fluxos
Existem dois fluxos na aplicação, o de compra e de venda de ações.
O fluxo de compra começa com um crédito em conta (Deposit
na WebApi), passa pela colocação de uma ordem (PlaceOrder
) que tem sua execução realizada pelo simulador de execução da bolsa de valores (Execute
) e termina com a possibilidade da consulta às ações compradas, que formam a posição do cliente (Positions
).
Já o fluxo de venda segue o caminho inverso. A partir das ações compradas coloca-se uma ordem de venda (PlaceOrder
), que também é executada pelo simulador da bolsa de valores (Execute
) e que gera um depósito na conta do cliente, o que lhe permite um saque (Withdrawal
).
A Web API vem com um Swagger que te permite executar todos os comandos e consultar seus efeitos colaterais.
Parte 1 - Eventos
Para aplicarmos o Event Sourcing criamos uma classe abstrata com o necessário para que todo e qualquer agregado consiga fazê-lo. Esta classe chama-se EventBasedEntity
. Veja o código abaixo:
namespace WSantosDev.EventSourcing.Commons.Modeling
{
public abstract class EventBasedEntity
{
public int Version { get; protected set; }
public List<IEvent> UncommittedEvents { get; } = new ();
protected void RaiseEvent<TEvent>(TEvent @event) where TEvent : IEvent
{
Version++;
UncommittedEvents.Add(@event);
ProcessEvent(@event);
}
protected void FeedEvents(IEnumerable<IEvent> stream)
{
foreach (var @event in stream)
{
Version = @event.Id;
ProcessEvent(@event);
}
}
protected abstract void ProcessEvent(IEvent @event);
}
}
Para facilitar a compreensão do código acima, vamos vê-lo em ação em nosso agregado Account e, em seguida, explicamos o conjunto.
...
public Result<IError> Credit(Money amount)
{
if (amount <= Money.Zero)
return Result<IError>.Error(Errors.InvalidAmount);
RaiseEvent(new AmountCredited(Version, amount));
return true;
}
...
protected override void ProcessEvent(IEvent @event)
{
switch (@event)
{
case AccountOpened accountOpened:
Apply(accountOpened); break;
case AmountCredited amountCredited:
Apply(amountCredited); break;
case AmountDebited amountDebited:
Apply(amountDebited); break;
}
}
...
private void Apply(AmountCredited @event) =>
_entries.Add(Entry.Credit(@event.Amount));
Repare que o código de Account
complementa o de EventBasedEntity
. Agora vamos às explicações.
Quando um método do nosso agregado é acionado, seus argumentos são checadas (no caso se o valor depositado é maior que zero) e, em caso positivo, um evento é lançado. Neste caso lançamos o evento AmountCredited
, via RaiseEvent
, que vai atualizar o estado do módulo.
Você pode estar se perguntando: mas não é mais simples apenas atribuir o valor do depósito a uma propriedade Balance
e seguir a vida? E a resposta é: não!
Veja, quando lançamos um evento, alimentamos a lista UncommitedEvents
que representa o conjunto de eventos que ainda não foi persistido em nossa Event Store. E, como veremos à frente, essa lista é o que dá sentido ao Event Sourcing, da mesma forma que a versão do nosso agregado.
Parte 2 - EventStore
Algo interessante a dizer sobre a Event Store é que ela é agnóstica a mecanismo de persistência. Você poderia utilizar um arquivo caso quisesse -- isso não é uma recomendação!
Em nossa demo utilizamos SQLite dado que bancos relacionais são a realidade da maioria de nós, mas poderia ser qualquer outro mecanismo de persistência que nos permita escrever uma versão serializada de nossos eventos, como vemos no código abaixo:
...
public sealed class EventDbContext(DbContextOptions<EventDbContext> options) : DbContext(options)
{
private DbSet<Event> Events { get; set; }
public void AppendToStream(string streamId, IEnumerable<IEvent> events)
{
var eventsToAppend = events.Select(e => EventSerializer.Serialize(e.Id, streamId, e));
Events.AddRange(eventsToAppend);
}
}
internal record Event(int Id, string StreamId, string Created, string EventType, string Data);
Nota: utlizamos EF Core por simplicidade. Seu uso não é uma necessidade, assim como não é necessário qualquer outro ORM. O importante é ter acesso ao mecanismo de persistência e fazer operações de escrita de cada evento.
Repare que no código acima, o método AppendToStream
espera um streamId
e um tipo enumerado de IEvent
, que é a lista UncommitedEvents
que temos em nosso agregado.
Repare também que os eventos são serializados para preencher a entidade Event
que será o registro de fato armazenado na base de dados.
Vamos ver como funciona essa serialização:
internal sealed class EventSerializer
{
...
public static Event Serialize<TEvent>(string streamId, TEvent @event) where TEvent: IEvent
{
return new(eventId,
streamId,
DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"),
@event.GetType().FullName!,
JsonSerializer.Serialize(@event));
}
}
Então você poderia me perguntar: mas por que serializar? Por que não persistir o agregado e seguir a vida? Repare que recebemos TEvent
como parâmetro, isso para podermos permitir que qualquer tipo de evento seja persistido na base sem que tenhamos de nos preocupar com sua forma (nos preocupamos apenas com seu tipo para permitir a desserialização no futuro). Deste modo é muito mais simples persistir e é exatamente essa simplicidade que nos dá a flexibidade para escolher qualquer meio para persistir nossos eventos.
Com este código conseguimos persistir os eventos que constituem o agregado com sucesso, precisando apenas recuperá-los em ordem (daí a importância do Version
em EventBasedEntity
) para restaurá-lo e poder operar com ele.
Parte 3 - Snapshot
Como dito acima, às vezes temos um ciclo de vida muito longo para um agregado. Uma conta, por exemplo, pode existir e ser movimentada por anos, o que, em grande volume de usuários, poderia comprometer o desempenho do sistema.
Assim sendo, podemos criar snapshots a partir do código abaixo, que nos permite definir um tipo que será persistido e poderá ser recuperado no futuro para facilitar a restauração do agregado.
Veja como funciona o snapshot pelo código de Account
public sealed partial class Account : EventBasedEntity, ISnapshotable<AccountSnapshot>
{
public bool ShouldTakeSnapshot() => _entries.Count % 3 == 0;
public AccountSnapshot TakeSnapshot() =>
new (AccountId, Version, [.. _entries]);
}
A interface ISnapshotable
nos fornece os dois métodos acima para decidir se devemos ou não criar um snapshot e para gerar o tipo de snapshot que será persistido.
Aqui usamos o tipo AccountSnapshot
que é uma representação do estado atual do agregado, neste caso seu id, sua versão e o conjunto de entradas que possui neste momento. Da mesma forma que um evento, este snapshot será serializado e persistido, com a diferença de que novos snapshots vão sobrescrever o original em vez de ser adicionados para viver junto com ele.
Parte 4 - Projeções
Aqui temos a criação de nosso modelo de leitura (ou view) que será consultado quando necessário pela nossa aplicação.
Nota: no caso específico de
Account
optamos por simular um modelo de consistência eventual, e digo simular porque, em vez de utilizarmos um mecanismo de mensageria (como o RabbitMQ) ou event stream (como o Kafka), para receber os eventos de domínio e convertê-los em projeções, utilizamos um bus em memória por simplicidade.
No caso de Account, sempre que um evento do Event Sourcing é lançado, o comando que operou sobre o agregado irá lançar um evento de domínio correspondente, que será interceptado por um manipulador de eventos de domínio que o converterá em uma projeção.
Nota: No caso dos demais módulos optamos por um modelo de consistência imediata. Em vez de lançarmos um evento de domínio correlato ao comando executado, persistimos a projeção na mesma transação em que persistimos os eventos do Event Sourcing (o que só é possível porque ambas as persistências estão na mesma base). Vale a pena examinar as diferenças entre ambas as abordagens.
Vamos examinar um desses manipuladores de eventos de domínio para entender como a projeção é criada.
public class AccountOpenedHandler(AccountViewDbContext viewDbContext) : IMessageHandler<AccountOpened>
{
public async Task HandleAsync(AccountOpened message)
{
var view = AccountView.Create(message.AccountId, message.InitialDeposit);
viewDbContext.Add(view);
await viewDbContext.SaveChangesAsync();
}
}
Veja que é um manipulador bem simples, que reage a abertura de uma conta. Ele apenas cria uma projeção da conta, com seu id e o valor de seu depósito inicial, e o persiste em uma base de views (que, em nosso caso, é a mesma base onde persistimos os eventos, mais uma vez por simplicidade).
Pronto! A partir de agora podemos consultar essa projeção para sabermos qual é o saldo de uma conta no momento da consulta.
Repare em algo interessante: essa view é uma versão customizada de nosso agregado e sequer é fiel a ele. Não existem entradas, apenas o id da conta e seu saldo. Isso porque é apenas esse dado de que precisamos na aplicação. Se precisássemos de um histórico de transações, um extrato, poderíamos criar uma segunda view que contivesse o número da conta, a data da movimentação, seu tipo (abertura de conta, crédito ou débito) e o valor.
Esse é o poder do CQRS: é possível criar qualquer visualização desejada a partir do estado da aplicação, inclusive para, como dito acima, a geração de relatórios que a aplicação não consuma e que sirva apenas a fins analíticos.
Excelente! Não?
Parte 5 - Juntando as peças
Agora que entendemos as peças fundamentais do Event Sourcing/CQRS, podemos examinar nossa Web API que é quem fornecerá nossas funcionalidades ao mundo.
Aqui encontraremos comandos e consultas que nos permitirão, a partir da nossa necessidade, obter dados das projeções ou restaurar o estado de nosso agregado para realizar operações.
Vamos ver um exemplo do controller de crédito em conta.
public class CreditController(Credit command) : ControllerBase
{
[HttpPost("Credit")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
public async Task<IActionResult> Credit(CreditRequest request)
{
var credited = await command.ExecuteAsync(new CreditParams(Constants.DefaultAccountId, request.Amount));
if (credited)
return Ok();
if (credited.ErrorValue is InvalidAmountError)
return BadRequest($"Invalid amount. The amount should be greater than zero.");
return StatusCode(StatusCodes.Status500InternalServerError, "Unspecified error.");
}
Aqui temos o seguinte: recebemos uma instância de um comando de crédito que encapsula toda a lógica necessária a essa operação. Ele restaura o agregado, invoca seu método de crédito, persiste e lança o evento de domínio correspondente. No controller temos apenas a execução do comando com seus respectivos parâmetros (o id da conta e o valor do depósito).
Se o comando for bem sucedido, retornamos um Ok (200)
, do contrário, se a falha ocorreu por conta da invariante do agregado (depósito com valor maior que zero), retornamos um Bad Request (400)
e, caso a falha seja de outra natureza, retornamos um InternalServerError (500)
.
Simples. Não?
Vertical Slices
Repare que nosso controller se chama CreditController
, ou seja, ele se responsabiliza apenas pelo depósito de valores na conta de um cliente. Essa abordagem, que é orientada a tarefas e não a recursos, se chama vertical slices (fatias verticais em tradução livre), e é uma forma de segregar as ações possíveis no software de acordo com seu propósito.
Ou seja, para verificarmos o saldo em conta de um cliente, teremos um BalanceController
, para realizar um débito na conta, um DebitController
e por aí vai. Isso em vez de ter um único controller que reúna todas as operações possíveis em Account, o que pode se tornar difícil de manter e evidencia a simplicidade do próprio design da aplicação (se um controller está muito grande, é possível que o design não esteja adequado à solução).
Aqui temos uma breve apresentação deste padrão segundo o livro (sempre em tradução livre): "Até onde sei o termo 'Vertical Slice Architecture' foi cunhado por Jimmy Bogard em uma série de artigos por volta de 2019. Seu principal mote é 'minimizar o acoplamento entre as fatias e maximizar o acoplamento dentro de uma fatia'".
Pode soar um pouco estranho ler o trecho "maximizar o acoplamento dentro da fatia", mas foi exatamente o que fizemos em nosso caso. Temos um controller apenas para a fatia de crédito, assim como um manipulador de comando e um evento de domínio. Todos esses elementos bastante acoplados entre si pois são todos dependências da funcionlidade de crédito.
Nota: assim como tratado no post CQRS - Desfazendo mal-entendidos, não há a menor necessidade de utilizarmos o famoso MediatR. Implementamos a injeção do manipulador de comando diretamente no controller pois nossa intenção é realizar uma única operação, o que acaba por tirar a razão de existir do MediatR que é notificar N manipuladores de forma desacoplada. Vale a pena evidenciar a ausência de ligação entre uma coisa e outra.
Conclusão
Como pudemos ver, Event Sourcing traz uma vantagem e um poder enorme para uma aplicação, nos permitindo conhecer por completo o histórico de mudanças de seu estado e, ao mesmo tempo, provendo uma forma de gerar visões distintas para propósitos específicos. É evidente que Event Sourcing não é uma necessidade para toda e qualquer aplicação mas, quando faz sentido, é um instrumento poderossímo e aliado do negócio -- que é a razão pela qual a aplicação existe.
Espero que tenha gostado desta releitura 5 anos depois e que se divirta com o código da aplicação de demonstração para conhecê-la em detalhes.
Se gostou, me deixe saber pelos indicadores do post e, caso queira, deixe um comentário, principalmente se tiver alguma dúvida.
Muito obrigado pela leitura e até o próximo post!