Com a popularização de arquiteturas baseadas em microsserviços, diferentes conjuntos de contextos e funcionalidades passaram a ser decompostos em unidades menores e autônomas, caracterizando cada uma destas unidades como um microsserviço.
Por exemplo, podemos imaginar uma aplicação para gestão de contas bancárias. Poderíamos construir essa aplicação através da divisão de contextos menores e independentes, o que facilitaria a manutenção e o gerenciamento dos processos de escalabilidade de cada contexto.
Como exemplo, vamos imaginar a seguinte divisão:
Com essa divisão, podemos tratar cada um destes contextos como um serviço dedicado, isolado e independente, possibilitando o isolamento das regras de negócio de cada contexto e a criação de serviços mais coesos.
Dessa maneira, podemos imaginar que nossa aplicação para gestão de contas bancárias poderia ser dividida em três microsserviços: microsserviço de extrato, microsserviço de gestão de saldo e microsserviço de usuários. Cada um desses serviços poderia funcionar de maneira completamente isolada dos demais serviços envolvidos, diminuindo o impacto no caso de falhas de um ou mais contextos.
Neste exemplo, provavelmente a aplicação bancária terá uma tela inicial onde serão exibidos dados que devem ser providos tanto pelo serviço de extrato quanto pelos serviços de saldo e usuário. E é aqui que o problema pode começar: como estamos utilizando microsserviços, estas informações não estão centralizadas em uma única fonte de dados, mas sim em três fontes de dados completamente distintas.
Para resolver esta situação, poderíamos delegar ao front-end o disparo em paralelo de três requests para cada um dos microsserviços com o intuito de obter as informações para serem renderizadas na tela.
Até que é uma solução que resolve o problema de certa forma, porém, colocando uma série de problemas do ponto de vista arquitetural, como:
• No caso de o front-end precisar fazer as três requisições em paralelo, há uma penalização do lado do cliente, pois é ele quem será responsável por realizar e coordenar estas três requisições para a montagem da tela ao invés de ser responsável unicamente por cuidar do processo de renderização da tela;
• Requisições em geral (como requisições REST HTTP) geralmente possuem um custo técnico considerável. Lidar com o processo para realização de conexões HTTP de maneira segura é geralmente complexo, sendo assim se torna interessante realizar a menor quantidade de requisições possível. Há também o custo monetário nesta questão, já que se imaginarmos um usuário abrindo a tela inicial da conta bancária em uma conexão 4G, por exemplo, os três requests que precisarão serem feitos farão com que o aplicativo de conta bancária consuma mais rapidamente o pacote de dados do cliente (custo que poderia ser diminuído se uma única requisição for feita);
• Pode haver também um desperdício de recursos neste cenário no caso de falhas, pois se um dos microsserviços não conseguir devolver a resposta esperada, a tela não poderá ser montada; porém, os demais requests continuarão a serem feitos de maneira desnecessária;
• Há uma mistura de responsabilidades: o front-end deixa de ser responsável unicamente por lidar com os processos de renderização e passa a lidar também com o controle de fluxo para a comunicação com os diferentes microsserviços. Isso torna o front-end mais acoplado com o back-end e dificulta a realização de testes, além de poder causar um aumento no tempo necessário para que o front-end seja capaz de finalizar a renderização da tela inicial em nosso exemplo (devido à sobrecarga de responsabilidades).
Mas então, como manter uma arquitetura baseada em microsserviços, porém, considerando que os dados necessários para renderização nos front-ends possam ser obtidos a partir de um ponto centralizado?
Curso React - Boas práticas
Conhecer o cursoO pattern BFF
BFF é um acrônimo para Backend For Frontend. Trata-se de um componente de back-end que tem a missão de prover os dados necessários para cada front-end da aplicação, agindo como um “meio de campo” entre os front-ends e os diferentes back-ends que fazem parte de uma aplicação baseada em microsserviços.
Tendo como base o exemplo anterior, ao invés do aplicativo de conta bancária realizar três requisições diferentes para cada um dos microsserviços para conseguir renderizar a página inicial, o aplicativo poderia fazer uma única requisição para um BFF. Este BFF é quem seria responsável na estrutura interna da aplicação por conhecer quais serviços precisam ser invocados para que os dados para montagem da tela inicial possam ser obtidos.
Essa abordagem resolve os problemas que existiam em delegar a responsabilidade pelas requisições para o front-end, além de tornar as fontes de dados completamente transparentes para os componentes que precisarem consumir esta informação.
Se em algum momento for necessário mover as informações de extrato para outro serviço (ou compor estas informações com chamadas para outros microsserviços), essa mudança fica completamente transparente para o aplicativo, pois basta realizar a alteração no BFF, fornecendo as mesmas informações esperadas pelo front-end a partir das novas interfaces.
É importante ressaltar que os BFFs são altamente orientados às interfaces e jornadas de usuário: sua única responsabilidade deve ser prover os dados necessários para a renderização das interfaces em diferentes front-ends.
Como diferentes front-ends podem ter necessidades de informação diferentes para cada cliente, é perfeitamente aceitável termos BFFs segregados até mesmo pelos conjuntos de diferentes clientes que irão consumir as informações.
No exemplo da conta bancária, poderíamos ter um BFF voltado para a interface web (pois provavelmente precisaremos de mais informações para renderizar a interface web) e outro BFF voltado para a interface mobile (que provavelmente precisará de menos dados para que as telas possam ser renderizadas).
Esta possibilidade de segregação por tipo de cliente que o BFF dá é essencial para que seja evitado um efeito chamado over-fetching.
Poderíamos criar um único BFF tanto para a interface web quanto para a interface mobile e deixar este BFF devolver as respostas da maneira como somente a interface web espera. Provavelmente, o payload necessário para a interface web é mais extenso, já que a interface web espera mais dados para que seja possível renderizar a página.
No caso, bastaria ao front-end mobile consumir estas mesmas informações, desprezando os dados adicionais que seriam retornados – dados estes que seriam utilizados somente pela interface web. Nesta situação, aconteceria um over-fetching: um dos clientes (no caso, a interface mobile) iria receber muitos mais dados do que o necessário para que pudesse ser renderizada.
O problema com o over-fetching é que ele acaba por penalizar os front-ends. No nosso exemplo, o aplicativo mobile precisaria de um esforço muito maior para entender uma resposta muito maior que a necessária devolvida pelo BFF unificado.
Uma parte desse esforço seria em vão, já que uma parte destas informações processadas simplesmente seria descartada por não ser necessária na interface do aplicativo. Além disso, o aplicativo seria penalizado com uma resposta do BFF unificado muito maior do que o necessário, o que causaria um consumo maior de largura de banda e dados por parte do aplicativo.