De tempos em tempos, algumas abordagens de desenvolvimento parecem virar padrão recorrente. Depois de algum tempo vendo empresas adotarem (as vezes, sem justificativa real) arquiteturas baseadas em microsserviços, estamos percebendo interesse crescente em padrões baseados em eventos.
Nesse, e em outros posts, vamos apresentar alguns conceitos chaves e padrões recorrentes para adoção de eventos no desenvolvimento de software. Comecemos com sistemas que empregam notificações por eventos.
Entendendo a ideia principal
Imagine um sistema onde, toda vez que um componente (módulo, microsserviço, etc) conclui uma operação, precisa inicar uma operação associada em outro componente.
Por exemplo, considere um sistema de e-commerce, onde, após confirmada uma venda, seja necessário disparar o processo de separação e remessa de um produto.
Uma abordagem ingênua simples, seria fazer com que o primeiro componente acione ativamente o segundo componente (chamando um método, fazendo um request para algum endpoint específico, enviando uma mensagem “comando”, etc).
Ainda em nosso exemplo, poderíamos considerar que o sistema de vendas “chamaria” o sistema de remessas com uma chamada a um endpoint, ou, em um cenário monolítico, executando um método.
Uma solução mais complexa sofisticada, entretanto, seria fazer com que o primeiro componente “disparasse” uma mensagem notificando a ocorrência de um evento de negócio. Caberia ao segundo componente”escutar” essa mensagem e realizar o processamento adequado. Nesse cenário, o primeiro componente acionaria passivamente o segundo componente.
Assim, ainda em nosso exemplo, poderíamos fazer com que o sistema de vendas “disparasse” um evento com dados relacionados a venda que acabou a ser realizada. Enquanto isso, o sistema de remessas “escutaria” esse evento e faria seu trabalho.
Essa abordagem possui o efeito colateral positivo de permitir que outros componentes “escutassem” um mesmo evento permitindo a adição de outros processamentos sem que exista a necessidade de alterar as partes que já estão funcionando no sistema.
Por que desenvolver sistemas com notificação por eventos?
Sistemas distribuídos (incluindo aqueles baseados microsserviços) estão ficando cada vez mais comuns. Nesses sistemas, um dos atributos arquiteturais mais importantes é o baixo acoplamento.
Da Wikipedia:
Em engenharia de software, acoplamento ou dependência é o grau de interdependência entre módulos de software; uma medida de quão intimamente ligadas estão duas rotinas ou módulos; a força das relações entre módulos.
O acoplamento é geralmente contrastado com coesão. O baixo acoplamento geralmente se correlaciona com alta coesão e vice-versa. O baixo acoplamento é frequentemente um sinal de um sistema de computador bem estruturado e de um bom design, e quando combinado com alta coesão, suporta os objetivos gerais de alta legibilidade e facilidade de manutenção.
Em termos práticos, queremos desenvolver, distribuir e manter sistemas, alterando suas partes, sem afetar as demais. A estratégia para comunicação entre componentes, empregando notificação por eventos, ajuda a desenvolver sistemas com baixo acomplamento.
Em 2002, trabalhei no desenvolvimento de uma solução CAD complexa. Um dos objetivos do projeto, na época, era desenvolver um sistema extremamente componível (queríamos poder instalar plugins no sistema, enriquecendo suas features).
Determinamos que cada vez que uma operação fosse realizada, ou houvesse mudança de estado, um evento fosse disparado (na época, chamamos de gatilho) em um “bus” rudimentar. Os “plugins” do sistema tinham acesso ao “bus” e conseguiam “identificar” quando um gatilho (evento) de um determinado tipo havia sido disparado e conseguiam então executar processamentos relacionados.
Tangibilizando, é como se no “word”, quando o usuário ativasse negrito usando atalhos de teclado, fosse disparado um “evento” que o botão na toolbar “escutaria”, se ativando.
Problemas em sistemas que empregam notificações de eventos
Baixo acoplamento é ótimo. Porém, tem um preço que pode ser bem alto.
Quando permitimos que componentes comecem a “escutar” eventos em nossa aplicação, é fácil perder o controle do que será executado.
No sistema desktop que descrevi acima, começamos a padecer com componentes que realizavam processamentos pesados como “resposta” a eventos e a aplicação, muitas vezes, perdia em responsividade.
Sistemas distribuídos, baseados em notificações por eventos, tornam alguns tipos de operações comuns (como transações) difíceis de implementar e, principalmente, monitorar.
Importante determinar que é possível conseguir um bom nível de baixo acoplamento simplesmente implementando abordagens assíncronas para acionamento ativo (como envio de comandos de um componente para outro usando algum mecanismo de mensageria).
Concluindo
Sistemas baseados em notificações com eventos tem, naturalmente, baixo acoplamento e tendem a ter evolução facilitada. Entretanto, é necessário estar atento a “evolução” dos fluxos de execução balanceando “complexidade” com simplificação da implementação das operações do domínio.
Sistemas baseados em eventos apresentam novos e complexos desafios para implantação de monitoramento (tema para outro post).
Você tem alguma experiência desenvolvimento sistemas que adotam eventos de notificação?
Um outro ponto importante nesse assunto é que eventos vendem a idéia de baixo acoplamento, mas se esquecermos que o conteúdo dos eventos também causam acoplamento, o objetivo de desacoplar não é alcançado, resultando em um acoplamento distribuído.
Exemplo comum, porém errôneo: Serviço A notifica que um cliente foi registrado. Serviço B escuta esse evento e salva o nome do cliente em seu banco de dados para ser usado posteriormente. Nesse momento, serviço B passa a depender do serviço A notificá-lo de que o nome do cliente foi alterado.
Em um cenário como esse, é importante manter os dados protegidos em seus contextos (serviços) evitando a dependência no conteúdo do evento.
Serviço A sabe o nome do cliente, porém só pública a informação de que um cliente X foi registrado. Serviço B registra a existência do cliente X, somente como referência e outros dados relacionados ao contexto do serviço B.
TL;DR
Uma dica muito boa que me ajuda a modelar sistemas distribuídos, quando necessário: Não vaze informação de negócio para fora do seu lugar de coesão.
Pensa na alegria da pessoa – eu – ao ler essa matéria e estar no meio do desenvolvimento de um sistema em node tentando dividir em Bounded Contexts e usando justamente a abordagem assíncronas para acionamento ativo com chamadas assíncrona em um pub/sub simples.
Não entendi…
Na sua solução, como o serviço B ficará sabendo se o nome do cliente for alterado?
Nunca? , ou pior, terá um ‘endpoint’ específico pra alterar o nome do cliente?
Poderia explicar, por gentileza?
Pq eu uso exatamente o cenário ‘errado’ que vc disse.
Obrigado.
Fala Alessandro,
A resposta curta para a sua pergunta é: O Serviço B não precisa saber que o nome foi alterado. O nome deve ser de responsabilidade de um e somente um serviço.
Exemplificando mais, me permita dar mais detalhes de um domínio de exemplo:
Serviço A seria o serviço de Carteira de Clientes
Serviço B seria o serviço de Faturamento
Digamos que temos uma feature a ser implementada: Integrar com a receita federal a nota fiscal de um pedido faturado.
E por lei, o nome do cliente é obrigado.
Nesse cenário:
– Serviço de Carteira de Clientes saberia qual o cliente/nome associado a um pedido.
– Serviço de Faturamento sabe quando um pedido é faturado
– Serviço de Faturamento comanda a integração de uma nota fiscal
– Serviço de Integração c/ Receita recebe essa requisição e obtém os dados necessários para nota fiscal
— Nome do cliente vindo da Carteira de Cliente
— Valor do pedido vindo do Faturamento
— Endereço de entrega vindo do serviço Z
– Serviço de Integração c/ Receita integra a nota fiscal com os dados compostos
Perceba que o nome do cliente nunca saiu do seu contexto e que Faturamento não tem nenhum motivo para manter o nome do cliente.
No exemplo da nota fiscal, o nome é necessário, e é obtido na forma de composição. Da mesma forma que em um relatório, o nome pode ser necessário e será obtido por meio de composição.
O acoplamento a uma mensagem é menor que o acoplamento a um serviço específico. Não? 🙂
Oi.. entendi perfeitamente sua explicação e agradeço..
Porém, me permita te dar uma sugestão: dê uma olhada em microserviços autônomos. Muito resumidamente, você deve trocar mensagens entre os serviços via mensageira de forma que quando um serviço for realizar o seu trabalho os dados já estejam no seu banco, sem precisar fazer requisição para outro serviço. Até porque, especialmente Se for requisição síncrona gera acoplamento.
No exemplo anterior quando digo requisição, quero dizer qualquer forma de comandar algo a outro componente.
Valeu pela sugestão, porém essa abordagem propõe replicar os dados onde ele for necessário. Caso esse dado seja alterado na origem, o custo de manutenção desse dado em todas as dependências é muito alto, sendo muito fácil de perder em quem é a autoridade de negócio responsável pelo dado. No final o que se tem é uma confusão de quem é dono de quem.
É importante desacoplar com mensageria, mas acho mais importante ainda desacoplar no nível da dependência do dado, pois é nesse nível que se protege os bounded contexts.
Ja mexi com backend distribuído dessa forma (sem pensar se faz sentido o dado se espalhar pra todo lado) e era comum dar desculpas em nome da consistência eventual (as vezes nem eventual chegava).
Sim, concordo. Mas botar uma mensagem no meio não é tudo.
Óbvio que não depender do SLA do outro componente já adianta muito, mas não é tudo 🙂
Arley.. só me restou uma dúvida na sua abordagem: eu só não consigo entender como vc fará uma requisição assincrona quando um serviço precisa integrar os dados de vários outros serviços para realizar seu processo (enviar nf para a receita no seu exemplo). Só vejo sendo feito via requisição síncrona. E entre ter uma requisição síncrona e um dado bem documentado de outro serviço (detalhe para o bem documentado) ‘representando uma entidade’ de outro serviço para que eu já tenha tudo que preciso para executar uma tarefa em um serviço, essa última me parece a melhor abordagem. IMHO. Abraço….
Me esqueci de um outro detalhe: grandes players usam essa abordagem mais ou menos como eu disse. Ex: o Elemar em uma de suas palestras deu o exemplo do Facebook. Quando vc pede pra trocar seu nome, ele te pede até 72hrs, salvo engano, para realizar o processo. Ou seja, em back ele está replicando os dados (seu nome alterado) em todos os seus posts, comentários etc.. etc….
Até porque quando ele lista seus posts e comentários, se ele tiver que fazer solicitação para outro serviço só pra pegar seu nome. seria inviável.
Perfeito, finalmente um comentário sensato vindo de um case real. Event Bus só desacopla nos artigos e nos diagramas dos arquitetos que ficam depois para dar manutenção.
Corrigindo: “Event Bus só desacopla nos artigos e nos diagramas dos arquitetos que NÃO ficam depois para dar manutenção.”.
Concordamos em discordar. Não dá para balizar o mundo apenas por suas experiências.
Não se trata, necessariamente, de eliminar o acoplamento. Mas, sim, em buscar alternativas de acoplamento mais baratas.
Concordo com você que algum acoplamento é preservado. Entretanto, concordemos que é um acoplamento menor.
Há um porém. Rede costuma ser um grande problema para performance. Se a demanda por um serviço for alta o suficiente, como indicado no próximo post da série (https://www.eximiaco.tech/pt/2019/08/12/event-carried-state-transfer/) pode valer a pena.
🙂 Concordamos. Não é tudo, mas é bastante, em muitos cenários.
Thiago, adoraria entender melhor sua perspectiva. Há um “mundo” de implementações nesse modelo. Honestamente, gostaria de entender o que te fez fazer essa generalização.
Em tempo, no texto do post há um exemplo onde indiquei uma implementação concreta onde eventos fizeram sim desacoplamento. O problema, se moveu para tracing .. mas a conversa é outra.
Em tempo, nossa recomendação de leitura dessa semana pode ajudar a ver algumas possiilidades interessantes.
https://www.eximiaco.tech/pt/2019/08/16/enterprise-integration-patterns-eip/
Acho que vai gostar -> http://vertx.io/
[]s