2 pontos por GN⁺ 2023-12-18 | 1 comentários | Compartilhar no WhatsApp
  • 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 status podem 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
  • 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

 
GN⁺ 2023-12-18
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

    • Indo nessa direção, mesmo quando você precisa de “viagem no tempo”, “recuperação de estado sobrescrito” ou “reinterpretação de eventos antigos”, às vezes apenas um log de auditoria é suficiente
      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
    • Este comentário é mais valioso que o próprio artigo
      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
    • Fico curioso se, em geral, seria algo como ter uma tabela com a definição {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...
    • Fico curioso onde encontrar um artigo detalhado sobre como montar um sistema assim
  • 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.

    • Um sistema chato e tradicional que funciona bem é uma ameaça para uma equipe de engenharia inchada que não tem o que fazer e fica lapidando itens para colocar no currículo.
      Esse é exatamente o “problema” que essa solução resolve.
    • Por exemplo, bancos de dados temporais fazem bastante sentido para dados financeiros.
      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.
    • Quase todos os dados armazenados em SQL, na verdade, se encaixariam melhor em um banco de dados de documentos, mas ninguém está acostumado com isso, então seguimos do jeito que está.
      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.

    • Não é tão simples assim. O fato de ser ou não requisição-resposta não é um critério para escolher event sourcing ou arquitetura orientada a eventos.
      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.

    • Durable functions têm o problema de falta de observabilidade.
      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

    • Há várias palestras do autor
      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
    • Há duas formas
      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
    • Não existe apenas um “estado atual”. Esse modo de pensar vem de centralizar tudo em um único DB
      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.
    • Faz mais sentido usar isso em streams específicos, com muitas mudanças e dados interessantes o bastante para ver o que aconteceu no meio do caminho
      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

    • Ótimo, faça isso até deixar de funcionar. A questão é não usar martelo em parafuso
  • 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

    • Em um nível abstrato, eventos também podem ser modelados como relações
  • 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

    • Em sistemas de event sourcing, o stream de eventos é projetado em modelos de leitura, e pode haver vários modelos de leitura: relacionais, séries temporais etc.
      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