DEV Community

Pedro Kiefer
Pedro Kiefer

Posted on

Arquitetura Escalável

Atualmente todos falam em arquiteturas e software escaláveis: "it webscales!"; mas você realmente precisa de tudo isso no dia a dia? Seu sistema recebe 1M req/s para justificar complexidades e abstrações desnecessárias? Provavelmente a resposta é não. Então começe do básico, garanta ótima qualidade desde o príncipio e se um dia for necessário atender 1M req/s será muito mais fácil refatorar o sistema.

"Ah, mas eu preciso fazer micro serviços, porque todo mundo faz e isso escala!" Beleza, faça micro serviços mas não faça femto-serviços (minha definição para um serviço que é absurdamente pequeno; femto é 10^-15, enquanto micro é só 10^-6.). Um serviço de processamento de fotos não precisa ser 10 serviços diferentes com 10 filas separadas. Faça um serviço que englobe todo o processamento e escale esse serviço. Fica mais fácil de manter, dá para manter na cabeça todo o sistema, o deploy fica mais simples. "Ah, mas daí é um monólito", não, não é, só é um conjunto mínimo de funcionalidades reunidas em um local.

Se vocês tem muitos micro serviços para compor uma funcionalidade fica muito díficil coordernar uma atualização no payload usado entre os serviços. Será que todos vão entender a mensagem nova? Será que preciso atualizar tudo ao mesmo tempo? Se isso for necessário, agrupe tudo sob um sistema só.

Código

Abstraia e crie interfaces somente do que faz sentido no momento, não gaste tempo e energia criando uma arquitetura mega flexível que nunca será usada. Se o código estiver simples e bem testado fica simples refatorar para adicionar mais possibilidades.

Algumas linguagens em nome de "arquiteturas enterprise" (algo para C-level achar bacana, eu acho?) acabam criando diversos padrões de projeto que geram só níveis de indireção e abstrações que são pouco úteis para a entrega de valor.

Configurações

Evite juntar configurações do sistema com regras de negócio. Por exemplo, se você tem um sistema dinâmico para facilitar que outros times desenvolvam serviços em cima, deixe em arquivos separados as configurações que fazem o sistema funcionar e as configurações que são do negócio. O intuito aqui é diminuir os problemas em caso de uma configuração errada. Se temos um arquivo só e quebramos a configuração podemos tirar do ar todo o sistema. Quando separamos podemos continuar servindo conteúdo stale até arrumarmos a regra de negócio. Pense sempre em dois planos: controle e dados.

Versione todas as configurações do sistema - exceto senhas e dados sensíveis - junto com o código fonte. Configurações são tão importantes quanto o código. Evite alterar configurações manualmente, crie pipelines de entrega adequados para fazer as mudanças necessárias a partir do repositório.

Dependências

Exemplo de caos de dependências

Evite dependências externas, especialmente as que você tem zero controle. Se você precisa usar serviços externos entenda do princípio que eles vão falhar e sua aplicação provavelmente não deveria falhar junto – claro, se for algo essencial da aplicação não dá para ficar sem. Mas um sistema de métricas ou de logging não deveria tirar a aplicação do ar. Nem um deploy em outro sistema deveria ter um impacto enorme na sua aplicação.

Use retentativas, circuit-breakers ou ainda service mesh, para facilitar a gestão das dependências. Se as aplicações estão muito acopladas, então não há benefícios de ter micro serviços e um grande monólito faria um trabalho muito melhor. Pense em micro serviços como peças que possam ser trocadas quando necessário — e talvez dê para continuar voando sem elas.

Exemplo

Para exercitar as ideias apresentadas vamos criar um caso de uso real: um sistema de vendas de ingressos para um cinema. O sistema consiste em usuários podendo escolher qual filme querem assistir, em qual dia e horário, e todo o fluxo de compra e emissão do ingresso. A arquitetura inicial é conforme a figura abaixo.

Arquitetura inicial

Essa arquitetura pode ser considerada um monólito. Uma única aplicação é responsável por todos os comportamentos do sistema: a autenticação dos usuários que desejam comprar ingressos, o sistema de pagamentos, a gestão de quais filmes estão sendo exibidos em quais salas, entre outras funcionalidades que desejarmos para um sistema como esse.

Você pode se perguntar: se isso é um monólito, como podemos afirmar que essa aplicação é escalável? Ninguém especificou qual o volume de acessos, quantas salas de cinema o sistema gerencia, nem quantos filmes diferentes estarão disponíveis e onde eles estarão.

Do ponto de vista de escalabilidade da aplicação, é perfeitamente aceitável começarmos com uma arquitetura dessas. No entanto, existe um pulo do gato para que o código não pareça um novelo de lã depois de um encontro com unhas afiadas: criarmos o sistema levando em conta os domínios necessários para seu funcionamento, garantindo que eles são independentes entre si e se comunicam atráves de interfaces bem definidas.

Arquitetura inicial interna

Nessa figura mostramos os domínios existentes, deixando claro o que está agrupado em cada um deles: autenticação, filmes, salas, ingressos, pagamentos. Se a arquitetura começa com uma boa separação de conceitos, fica fácil escalar. E, dependendo do contexto, essa arquitetura é a única necessária! Se formos pensar em uma cidade com poucos habitantes, que possui um único cinema com 4 salas que exibem apenas 4 filmes, temos quase certeza de que nunca teremos um volume de acessos maior do que esse sistema consegue aguentar.

No entanto, vamos exercitar nosso raciocínio para o outro lado. O sistema foi um sucesso, revolucionou a gestão de ingressos na cidade. A empresa, obviamente, quer estender o lucro e o sucesso obtido com o software. Para tanto, decidiu criar outro sistema para vender artigos relacionados a cinema.

De modo a facilitar o uso para os atuais usuários, resolveram ter uma solução única de autenticação. Como essa responsabilidade já estava totalmente separada na estrutura do código, bastou um refactor para tirar a gestão de usuários do sistema de ingressos e criar um sistema separado. Agora esse sistema pode atender os fluxos de venda de ingressos e de souveniers. Qualquer melhoria na gestão de usuários é propagada para todos os sistemas que o utilizam, e também conseguimos escalar só essa parte do sistema se precisarmos.

Arquitetura com autenticação separada

O sucesso foi estrondoso! A empresa continuou faturando e, logo em seguida, surgiu uma grande oportunidade de negócio: comprar outras salas na cidade vizinha. Além disso, em uma pesquisa de satisfação com os seus clientes, a empresa viu que a grande dor de seus usuários era uma falta de lugares marcados nos ingressos.

O sistema atual não dava conta - era preciso escalar melhor suas partes internas. Como os domínios não mudaram, basta uma reorganização e criação de novos subsistemas responsáveis por uma dada área. A gestão de ingressos ganha seu próprio subsistema, que escala independemente da gestão de salas e filmes. Lá também temos toda a lógica necessária para gerir a escolha de assentos, o tempo máximo de uma reserva, etc.

Arquitetura final para a história presente

Podemos ver como o sistema cresceu, outra fontes de dados surgiram, pequenas partes tornaram-se escaláveis. O sistema está pronto? Provavelmente não, sempre haverá novas oportunidades de negócio: aumentar o alcance de cidades, incorporar a gestão de teatros; depende da evolução do negócio. Mas tendo os domínios bem separados, conseguimos escalar na medida certa para não gerar sistemas super complexos. Internamente alguns domínios podem ainda se desdobrar em mais partes, mas o ponto principal é conseguirmos ver a arquitetura como um todo. Qualquer pessoa consegue manter um modelo mental conforme a última figura.

"Ah, mas esse exemplo é todo de backend, não dá pra aplicar em front." É possível sim, basta imaginar que temos todo o código de front-end como uma Single Page Application (SPA). Ter uma SPA é perfeitamente aceitável e permite o compartilhamento de componentes entre páginas, poupando o retrabalho! No entanto, imagine que essa SPA faz o roteamento para todas as páginas e componentes da aplicação - autenticação, pagamentos, visualização dos filmes disponíveis, escolha de sala, entre outras.

À medida em que a necessidade do sistema vai evoluindo, o número de páginas, componentes e comportamentos complexos vai crescendo. O desempenho e agilidade da página ficam comprometidos; a experiência é degradada para o usuário, que fica esperando até que todo o programa seja executado pelo navegador. Podemos pensar em separar em partes a aplicação, só carregando o necessários conforme a necessidade. Se o usuário nunca entrar na parte de pagamentos, por que gastar tempo deixando ela disponível?

O mesmo conceito e ideia de micro serviços pode ser aplicado a micro frontends, que entregarão pequenos comportamentos ou componentes que serão adicionados apenas quando estritamente necessário para a página, melhorando o desempenho e a experiência do usuário. Esses componentes podem ter a atenção devida de UXs e desenvolvedores dedicados que cuidarão e melhorarão a experiência, o que faz com que todos ganhem - usuários e empresa.

Top comments (1)

Collapse
 
dpereira profile image
Diego Pereira • Edited

"No entanto, existe um pulo do gato para que o código não pareça um novelo de lã depois de um encontro com unhas afiadas: criarmos o sistema levando em conta os domínios necessários para seu funcionamento, garantindo que eles são independentes entre si e se comunicam atráves de interfaces bem definidas."

É bastante comum observar como princípios mais básicos de desenvolvimento de software, que se aplicam desde os níveis menores aos maiores, como manter alta coesão, baixo acoplamento, separação de responsabilidades, etc., são frequentemente atropelados por equipes de desenvolvimento que decidem focar no uso de conceitos muito mais especializados e mais difíceis de implementar de uma forma produtiva já em uma primeira versão.

Também é comum ver o contrário: projetos já com um grande histórico acumulado mas apresentando ainda estruturas incrivelmente simples e já há muito tempo inadequadas segurando interações complexas construidas em cima, gerando gargalos tanto de performance quanto de produtividade, por exemplo.

O mais assustador é que é também bem comum ver facetas dos dois problemas em um mesmo projeto: estruturas complexas construídas para suportar features que nunca foram necessárias, coexistindo ao lado de estruturas simplistas e hacks feitos pra resolver necessidades reais pro qual o grande design genial inicial não passou nem perto de cogitar.

Muito bom o texto! :)