Da transição de dados relacionais para eventos
(event-driven.io)- O modelo relacional de CRUD mostra bem a estrutura de armazenamento, mas tende a sobrescrever o processo de negócio, dificultando rastrear o que realmente aconteceu no sistema
- Event Sourcing registra eventos imutáveis gerados após cada ação em um Event Stream, e depois lê essa lista para decidir o estado atual em decisões futuras
- A modelagem começa encontrando primeiro os eventos, depois conectando comandos (commands) e regras de negócio para entender o processo
- Ao buscar candidatos a eventos em dados relacionais existentes, vale observar colunas de estado, colunas de data, nullable e relações 1:N, mas é arriscado supor que apenas os valores de estado permitem reconstruir um histórico completo
- Ao migrar dados em que só restou o estado final, é mais realista começar com um evento de importação explícito como Order Imported do que tentar reconstruir à força eventos passados, validando repetidamente em um ambiente seguro
Enxergando dados CRUD sob um modelo centrado em eventos
- O modelo de dados relacional mostra quais dados são armazenados, mas dificulta entender o que aconteceu dentro do sistema e como os processos interagem
- A abordagem CRUD tradicional pode perder informações de negócio importantes ao sobrescrever dados
- Event Sourcing prioriza a qualidade da informação acima do tamanho do armazenamento e salva como eventos os fatos ocorridos após cada ação
Modelo básico de Event Sourcing
- Um evento é um fato sobre algo que já aconteceu e, uma vez armazenado, é um dado imutável que não pode ser alterado
- Event Stream é a lista ordenada de tudo o que aconteceu com um registro
- Não é possível alterar eventos passados, mas é possível corrigir erros anteriores adicionando um novo evento ao final
- Na hora de tomar decisões, lê-se a lista de eventos para determinar o estado atual e a próxima ação
Ordem da modelagem do processo
- A modelagem começa primeiro pela descoberta de eventos
- Depois, identificam-se os comandos (commands) para definir a intenção de executar uma determinada ação
- Por fim, organizam-se as regras de negócio
- Os eventos se tornam o eixo central para que equipes técnicas e de negócio entendam o processo em conjunto
- Com abordagens como o EventStorming de Alberto Brandolini, é possível entender o processo observando eventos, comandos e regras em conjunto
Encontrando candidatos a eventos em dados relacionais existentes
-
1. Observar colunas de estado
- Os valores de uma coluna
statuspodem refletir etapas do ciclo de vida dos dados - Se um pedido tem estados como initiated, shipped e paid, cada um deles pode se tornar candidato a eventos como Order Initiated, Order Shipped e Order Paid
- Ainda assim, valores de estado podem ser uma interpretação achatada do processo de negócio, então não se deve supor que são completos
- Deve-se evitar nomear eventos com ações CRUD, como Order Created, Order Updated e Order Deleted
- State Obsession é apresentado como um padrão a ser evitado
- Os valores de uma coluna
-
2. Verificar colunas de data
- Colunas de data podem indicar momentos importantes de ocorrência no ciclo de vida do processo
- CreatedDate e ModifiedDate dizem pouco, mas ShipmentDate, DeliveryDate e OrderPlacementDate oferecem pistas melhores
- Exemplo:
- ShipmentDate pode indicar a introdução do evento Order Shipped
- OrderPlacementDate sugere que Order Placed pode ser um nome melhor do que Order Initiated
- DeliveryDate mostra que talvez seja necessário um evento Order Delivered
- Essas pistas precisam ser validadas com especialistas do domínio para se alinhar ao processo real de negócio
-
3. Analisar se a coluna é nullable
- Colunas non-nullable são dados que sempre precisam ser fornecidos
- Colunas nullable podem ser preenchidas depois em outra ação ou serem opcionais
- Se forem obrigatórias no Ordering Process, esses dados também devem estar presentes no primeiro evento Order Initiated
- Um único tipo de evento nem sempre é o ponto de início de um stream; pode haver vários eventos iniciais
-
4. Procurar tabelas com muitas relações 1:N
- Para encontrar limites de stream, é possível começar analisando tabelas com muitas relações 1:N
- Tabelas que concentram muitas relações do lado “one” são candidatas a tipos de stream
- Também é preciso avaliar logicamente se os dados podem existir de forma independente uns dos outros
- shipment pode ser um processo separado de order
- order line dificilmente existe sem order
- Ao discutir esses limites, é possível descobrir mais eventos e ampliar o entendimento do processo
Não criar eventos falsos na migração
- Dados relacionais representam um estado final achatado, então tentar adivinhar os eventos detalhados do passado apenas a partir desse estado pode falhar ou gerar resultados imprecisos
- Em vez de forçar a criação de eventos históricos pequenos, é melhor fornecer explicitamente um evento como Order Imported, contendo o estado atual completo e o código de interpretação
- Um evento de importação deixa claro como os dados entraram no sistema e pode ser importante para troubleshooting e diagnóstico
Validar com protótipos
- A migração deve ser testada como protótipo em um ambiente seguro para verificar como o modelo realmente se comporta
- É preciso ajustar iterativamente comparando os resultados com o esperado
- Em vez de correr, é necessário preservar as informações existentes e, com base nelas, melhorar o modelo depois
- A estratégia geral de migração de dados relacionais para um modelo baseado em documentos também se relaciona com General strategy for migrating relational data to document-based
1 comentários
Opiniões no Hacker News
2c: Se você também precisa de PostgreSQL em outras partes do app, é melhor armazenar os dados de eventos também no PostgreSQL + ferramentas FOSS de relatórios (Apache Superset, Metabase etc.) e aguentar até cerca de 2 TB
Depois disso, dá para decidir se é preciso manter os 2 TB inteiros online ou se resumos por dia/hora bastam. Se for o segundo caso, continuar com PostgreSQL é mais do que suficiente[1]
Um cliente processa mais de 10 TB, 1.500 eventos por segundo, 600 bytes por registro (80 GB por dia antes da indexação), mantém online apenas 2 dias de dados detalhados, resume o restante e move os detalhes para o S3, onde continuam consultáveis via Athena SQL[2]
Incluindo até um portal de relatórios para clientes, o custo total fica abaixo de 2 mil dólares, e em um AWS RDS Multi-AZ com failover automático (db.m7g.2xlarge), tanto as inserções quanto as consultas de relatório rodam com carga abaixo de 2%. Como a equipe de negócios cria os gráficos diretamente, 1 engenheiro gasta menos de 5 horas por mês em manutenção
Com ferramentas proprietárias, alguns gráficos vêm “prontos”, mas usando pgsql os dados ficam em um só lugar; há um único sistema para aprender, um único sistema para manter online/replicar/fazer backup/recuperar, um único sistema para proteger/escalar, um único fornecedor para gerenciar, e milhões de engenheiros conhecem esse sistema
Em sistemas como Preset ou Metabase, dá para criar 12 gráficos em uma hora, e pessoas não técnicas também conseguem fazer isso
Para referência, apesar do meu viés, vi sistemas de banco de dados e relatórios surgirem e desaparecerem por mais de 20 anos, e o bom e velho PostgreSQL melhora a cada ano
https://instances.vantage.sh/aws/rds/db.m7g.2xlarge?region=u...
[1] Se realmente for necessário, também há sistemas compatíveis com PostgreSQL para escalar mais. Aurora escala algo como 3–5x, TimescaleDB 10x, CitusDB 10x+. Cada um vem ao custo de se tornar um pouco não padrão, então eu não recomendaria antes de realmente precisar
[2] O dashboard de relatórios para clientes precisa de respostas abaixo de 1 segundo, e o PostgreSQL entrega isso consultando tabelas de resumo indexadas. O Athena responde em cerca de 1–2 segundos com varreduras paralelas
Basta manter snapshots dos dados antes de salvar, ter scripts para identificar e coletar uma determinada sequência de eventos, e então uma pessoa revisa e aplica retroativamente em massa o efeito da nova lógica
Ferramentas como https://django-simple-history.readthedocs.io/en/latest/ são uma solução simples e razoavelmente confiável para criar tabelas de auditoria; se também for preciso auditar acesso direto ao banco de dados, dá para adicionar triggers no Postgres
Em teoria, gosto de event sourcing, mas, na prática, há boilerplate demais para adicionar novos fluxos CRUD ou para fazer intervenções e hotfixes de forma rápida e confiável em situações inesperadas, algo que startups em estágio inicial a intermediário precisam fazer com frequência
Se não for algo como implementar trilhos de processamento de pagamentos, event sourcing pode não ser a escolha certa
Em https://news.ycombinator.com/item?id=17817375 (2018) também há uma boa conversa sobre as desvantagens de event sourcing
O único problema do PostgreSQL é que ele tem alguns problemas de escalabilidade interessantes no lado das inserções. Normalmente, recomenda-se colocar uma fila entre a fonte de eventos e o DB
{id:uuid,created_at:timestamptz,data:jsonb}Especialmente quando a estrutura dos eventos varia e as definições dos eventos vão mudando, fica difícil aproveitar bem os recursos de índices JSONB
Talvez eu precise me aprofundar mais neste documento: https://www.postgresql.org/docs/current/datatype-json.html#J...
Há algum tempo, minha equipe considerou seriamente usar event sourcing, mas para mim parecia uma solução em busca de um problema.
Talvez pudesse ter funcionado para nós, mas os benefícios não ficaram imediatamente claros, e os riscos e a tentativa e erro de adotar uma nova abordagem não pareciam ser o melhor para o projeto ou para a empresa, então acabamos desistindo.
Talvez tenha sido uma decisão como a de uma ferramenta que perdeu uma oportunidade de aprendizado, mas não me arrependo de não ter entrado nessa toca do coelho quando nem havia uma raposa correndo atrás de mim.
Esse é exatamente o “problema” que essa solução resolve.
Mas, na maioria dos casos, basta usar um banco de dados comum e armazenar o histórico de alterações em tabelas auxiliares. Assim, o banco de dados principal funciona como uma espécie de visão materializada.
Não tenho grandes reclamações, e nem acho que tenha sido necessariamente uma escolha errada, mas surgem problemas na forma de lidar com mudanças no modelo de dados.
Parece que a maioria das formas de armazenamento de dados não acompanhou a maneira como o software é feito hoje, e coisas como eventos e filas são resultado de empilhar as funcionalidades necessárias sobre sistemas existentes.
Hoje, muitas relações entre dados acontecem entre vários serviços, ou seja, fora do banco de dados. É assim que se parece o ambiente moderno de TI em muitas organizações.
Há dados mestres internos que dão suporte a várias equipes de negócio e interagem com mais de 300 sistemas e aplicações de TI para simplificar o trabalho.
Com microsserviços, é mais fácil manter a lógica de negócio e o modelo de dados limpos, mas, em troca, é preciso gerenciar eventos, filas, estado dos dados e armazenamentos dependentes; hoje isso está complexo demais.
Gosto de SQL, mas, sinceramente, os sistemas que construímos hoje em dia provavelmente seriam quase todos bem atendidos se fossem colocados em SQLite.
O que falta nessas discussões é quando a arquitetura orientada a eventos é apropriada.
Em resumo, se o cliente fez alguma coisa e espera uma resposta, isso não é orientado a eventos; é simplesmente requisição/resposta.
Orientação a eventos é quando algo acontece fora de banda. Por exemplo, quando você faz push de código no GitHub e isso dispara uma build.
Nesse exemplo, atualizar a página para ver o código atualizado é requisição/resposta, mas a build de CI que entrou na fila é orientada a eventos.
Espero que ajude.
Mesmo em event sourcing ou arquitetura orientada a eventos, é possível criar fluxos de requisição-resposta, inline, bloqueantes e circulares.
Por outro lado, mesmo sem event sourcing ou arquitetura orientada a eventos, é possível criar bem assincronia com abordagens como workers, filas, atores e multithreading.
Modelar eventos de domínio é útil para explicar o problema que se quer resolver junto com especialistas de domínio, e talvez o ideal seja deixá-los documentados ao planejar a solução.
Se for para implementar de fato um sistema que forneça uma trilha de auditoria para uma máquina de estados de longa duração, é bem provável que seja melhor usar algo como Temporal.io ou durable functions.
Essas ferramentas usam event sourcing para a persistência interna e oferecem um modelo de programação que adiciona restrições diferentes ao código que orquestra funcionalidades (workflows) e ao código que interage com o mundo real (activities), forçando você a pensar em deduplicação e idempotência.
Gostaria de ouvir sugestões sobre como superar esse problema.
O conceito é interessante, mas o texto não explica bem como funciona
Fico curioso sobre como reconstruir o estado atual de forma eficiente a partir do stream de eventos, e como modelar o stream de eventos no banco de dados
https://www.youtube.com/watch?v=gG6DGmYKk4I
https://www.youtube.com/watch?v=jnDchr5eabI
https://www.youtube.com/watch?v=ArcypYS5XBQ
https://www.youtube.com/watch?v=uODSwR2CIV4
Ele também mantém exemplos no GitHub
https://github.com/oskardudycz/EventSourcing.NetCore
https://github.com/oskardudycz/EventSourcing.NodeJS
https://github.com/oskardudycz/EventSourcing.JVM
A primeira é usar um banco de dados projetado para esse tipo de uso. Google BigQuery, Amazon Redshift, ClickHouse etc.
Todos os dados atuais são, em essência, algum tipo de agregação. Em outras palavras, são como uma consulta com group-by sobre um banco de eventos
Se você tem os eventos, faz sentido, porque tecnicamente consegue recriar o estado atual ou estados passados com consultas de agregação
A segunda é renomear o armazenamento relacional e chamá-lo de camada de cache ao lado do sistema de eventos
Funcionalmente é a mesma coisa, mas não acende o alerta de quem insiste que tudo precisa ser orientado a eventos
A arquitetura descrita no texto existe de verdade. Só que é extremamente complexa, então os serviços que a aproveitam normalmente fazem coisas bem direcionadas. Pense em algo como Google Analytics, Datadog e Splunk
Você pode criar estados diferentes em sistemas diferentes, conforme requisitos diferentes
Se você estiver criando um sistema de compras, com compras e clientes, um serviço pode ler os eventos e criar tabelas relacionais para fins financeiros
Outro serviço pode ler os eventos e criar um armazenamento chave-valor de dados de clientes, e um terceiro serviço pode operar um serviço OpenSearch para busca de produtos
O stream de eventos é uma lista. Se você usa algo adequado ao propósito, como Kafka, ele vira várias listas, ou seja, tópicos, partições etc.
Mas isso também pode ser resolvido dentro do modelo relacional
Isto é a diferença entre top-down vs bottom-up, ou entre sob medida vs genérico
Top-down parte do domínio de negócio e mapeia a implementação sobre as tecnologias, ferramentas e fornecedores disponíveis
Bottom-up parte das tecnologias, ferramentas e fornecedores disponíveis e os monta para criar uma solução que funcione
No sob medida entram DDD, CQRS/ES, Sagas, TBUI (UI baseada/orientada a tarefas), GraphQL, tipos de dados algébricos etc.
No genérico entram RDBMS, CRUD, REST, transações ACID, CDC, UI administrativa genérica, no-code/low-code, tipos limitados/genéricos etc.
Vou continuar usando os bons e velhos dados relacionais
Concordo com arquitetura baseada em eventos, mas este texto parece ter dificuldade para transmitir o ponto
Eu focaria na diferença entre relações de dados e ações de negócio
Quando você começa a pensar em termos de ações e atividades de negócio, o movimento para longe de armazenamentos relacionais operacionais fica muito mais claro
Event sourcing tem muitas propriedades boas, então é interessante
Mas ainda não precisamos de relações? Se sim, como essas relações são implementadas?
Se a resposta for “tudo está implicitamente no código da camada de aplicação”, é difícil aceitar
Ainda assim é necessário consultar relações, manter uma visão relacional atualizada, ou algo parecido
Tudo bem se as relações não forem o núcleo do modelo de persistência, mas elas precisam ser implementadas em algum lugar da camada de dados, e não vejo isso mencionado aqui
O Firestore tem o mesmo problema. Todo mundo lida com relações de algum jeito, mas no fim vira um código de aplicação espaguete que não escala
Se você está familiarizado com programação funcional, isso é essencialmente igual a uma operação fold que reduz um stream de eventos a um estado
Pela minha experiência anterior com sistemas de event sourcing, há a vantagem de ter um histórico de eventos armazenado explicitamente, mas a complexidade também aumenta bastante
Surgem questões como como criar de fato o modelo de leitura, como gerenciar versões do modelo e se deve haver snapshots do modelo de leitura
Na minha experiência, na maioria dos contextos em que esse padrão foi aplicado, a complexidade extra não valeu a pena
O que é necessário é uma fila de comandos. Eventos de comando não são eventos de domínio