1 – O contexto
O ano era 2018. A Beep tinha pouco mais de 50 colaboradores e crescia de maneira acelerada. A equipe de tecnologia era formada por 5 desenvolvedores, que estavam desde a concepção do negócio. Como qualquer startup em sua fase inicial, boa parte da base de código foi criada em torno de muita experimentação. Antes de encontrar o tão sonhado product market fit, o negócio mudava em questão de meses ou, até mesmo, em poucos dias. O resultado desse processo foi ter um legado sem muita sofisticação no quesito design. Além disso, em determinado momento, esse código começou a afetar nossa velocidade de mudança, principalmente por conta do acoplamento exagerado que existia. Depois de uma série de tentativas, o negócio parecia ter encontrado, finalmente, seu product market fit ideal e começava crescer ainda mais rápido. Como a maioria da operação da Beep é suportada por tecnologia, precisávamos nos adaptar ao novo cenário. Fizemos um levantamento dos problemas que estávamos enfrentando inicialmente e chegamos a uma lista parecida com essa:- Dificuldade de mudança – se alterávamos uma parte do código, quebrávamos a outra;
- Dificuldade de adicionar novos membros no time – novos membros teriam de conhecer a base como um todo;
- Número de bugs aumentando devido às mudanças rápidas que precisavam acontecer.
2 – Microservices na Beep
2.1 – Arquitetura inicial
Antes de implementarmos microservices na Beep, contávamos, basicamente, com dois grandes sistemas:- Agendamento, responsável, principalmente, pelas vendas e acompanhamento dos atendimentos diários;
- Operação, chamado carinhosamente de Oswaldo pelo nosso público interno.
2.2 – Extraindo o primeiro Microservice
A gestão das equipes de atendimento era feita diretamente no Oswaldo e foi justamente esse o primeiro componente a ser extraído. Depois dessa alteração, o desenho da arquitetura ficou assim: Resultado: o diagrama ficou ligeiramente mais complicado que o anterior. Agora, temos um conjunto mais amplo de relações entre os sistemas que precisamos monitorar. Essas relações estão representadas pelas setas, que são divididas em sólidas – indicam uma comunicação síncrona entre os componentes – e tracejadas – indicam uma comunicação assíncrona. Boa parte da comunicação entre esses componentes é feita de forma assíncrona, publicando eventos diretamente em um broker de mensagens, que, no nosso caso, é o RabbitMQ. Durante a implementação desse primeiro componente, já notamos algumas dificuldades que não tínhamos no modelo anterior. Isso ficou mais evidente, inicialmente, na parte da infraestrutura. Vários questionamentos tiveram de ser respondidos quando resolvemos separar o código desse componente. Alguns deles não faziam sentido antes da separação, então, tivemos que lidar com algumas situações praticamente do zero:- Onde será feito o deploy desse componente? Uma instância nova EC2?
- Como será feito o deploy? Vamos utilizar Containers? ECS? Kubernetes?
- As APIs que estão expostas precisam de segurança?
- Precisamos lidar com o versionamento dessas APIs, mesmo para uma aplicação interna nossa?
- Como lidar com as transações que eventualmente podem ser distribuídas?
- Como implementar o monitoramento dessa aplicação? Como monitorar traces distribuídos em várias aplicações?
Don’t distribute your objects.
Apesar de já termos alguma experiência com objetos distribuídos e saber que esses obstáculos eram comuns, resolvemos a maioria deles, atrasamos a decisão de outros e optamos por seguir com a estratégia. Uma vez que esse microservice já estava no ar, começamos a colher alguns frutos interessantes:- A lógica de disponibilidade, agora, estava totalmente isolada;
- Adicionar ou alterar alguma funcionalidade seria possível com o conhecimento somente daquele domínio;
- Era mais fácil ter uma cobertura de testes, pois a aplicação foi construída de maneira totalmente isolada do framework.
2.3 – Extraindo a gestão de estoques para marketplace
Com os bons resultados do primeiro microservice, resolvemos seguir com a extração de mais um componente, que, em teoria, seria de baixo risco e isolado. Naquela época, a Beep tinha uma modalidade de vendas baseada em marketplace: uma clínica poderia publicar sua agenda em nosso aplicativo. Isso permitia que o cliente agendasse sua vacinação e fosse até uma clínica tomar a vacina. Um problema muito importante, que precisava ser resolvido nessa modalidade de vendas, era quantificar as vacinas que poderíamos vender. Ou seja, de alguma forma, era necessário que controlássemos o estoque das clínicas disponíveis no marketplace. Assim como a gestão de equipes, a gestão de estoques do marketplace também era responsabilidade do Oswaldo. Identificamos e deixamos explícito o Bounded Context, que cuidava do controle desse estoque, e extraímos para um novo microservice. O desenho da arquitetura ficou assim: Com a introdução do novo componente, podemos observar um aumento nas relações entre eles, com mais linhas sólidas e tracejadas aparecendo. Isso, no fim das contas, se traduz em complexidade. Esse componente, em especial, tinha uma característica muito mais transacional que o anterior: para toda venda que acontecia no marketplace, um evento era lançado e o componente era responsável por tratar aquele evento, para que, em um determinado momento, algum item fosse removido do estoque. O problema de controle de estoque já tem seus desafios habituais, então lidar com ele em um cenário distribuído começou a ficar ainda mais desafiador. As transações distribuídas ganharam nossa atenção. Antes, todo o processo de atualização da saída de produtos era feito no mesmo sistema de agendamento. Isso facilitava muito porque, caso algo desse errado, um rollback na transação era suficiente para solucionar o problema. Agora, com as responsabilidades divididas entre 3 componentes distribuídos, o rollback não era mais tão simples assim. Se um dos componentes falhar, dependendo de onde essa falha acontecer, desfazer esse processo pode ser muito trabalhoso. Imagine o exemplo:- Cliente seleciona 3 vacinas diferentes e confirma o agendamento
- O sistema de agendamento inicia as validações do processo para confirmar o agendamento
- É verificado se a data ainda está disponível
- Confirma-se o pagamento
- Os eventos de agendamento confirmado são lançados
- Os sistemas interessados (inclusive o componente de estoque) reagem aos eventos e executam suas tarefas
2.4 – A realidade
Com a implementação desses dois componentes, aprendemos rapidamente que existe uma série de desafios para uma implementação bem sucedida de microservices. Alguns dos problemas que percebemos além dos que listamos aqui:- Monitoramento é fundamental para entender o que está acontecendo em cada componente;
- Logs passam a ser um problema distribuído também – e você precisa deles;
- Debugar um problema que passa por vários componentes – que podem executar de maneira assíncrona – não é uma tarefa trivial;
- Ter alguém dedicado a tarefas de infraestrutura, um DevOps ou alguém que faça esse papel é muito importante para o sucesso da implementação.