Microsserviço 14
Nos últimos anos, as empresas começaram a abandonar a ideia de que o mundo web e o mundo mobile deveriam ser tratados de modo distinto, passando a pensar no mundo digital de forma mais holística.
Assim, as interfaces de usuário devem ser vistas como o lugar em que entrelaçamos os diversos fios das funcionalidades que queremos oferecer aos nossos usuários. Nesse contexto, a tradicional arquitetura em camadas costuma atrapalhar a entrega eficaz de software: quando a UI e o backend são responsabilidade de equipes distintas, cria-se uma necessidade constante de coordenação para realizar mudanças, com trabalho sendo repassado de uma equipe para outra.
Uma abordagem mais interessante é ter tanto a UI quanto o backend sob a responsabilidade de uma mesma equipe, o que facilita as mudanças e a adição de novas funcionalidades. Equipes com responsabilidade total são incentivadas a manter um ponto de contato direto com o usuário final do software, evitando aquele cenário em que as equipes de backend perdem a noção de quem é o usuário final.
Ainda hoje, porém, muitas organizações mantêm equipes dedicadas de frontend. Os principais motivos são a escassez de especialistas, o desejo de consistência e os desafios técnicos.
- Escassez de especialistas: entregar uma interface de usuário exige habilidades específicas, como conhecer aspectos de interação e de design, além do know-how técnico necessário para oferecer uma ótima experiência na web ou no mobile.
- Desejo de consistência: uma UI com aparência consistente passa a impressão de ser uma entidade única e coesa.
- Desafios técnicos: pode ser difícil trabalhar com determinadas tecnologias de interface de usuário em um sistema que não seja monolítico.
Equipes alinhadas a fluxos
Manter equipes dedicadas de frontend costuma ser um erro quando o objetivo é otimizar o throughput, pois isso cria novos pontos em que o trabalho precisa ser repassado para outras equipes. O ideal é alinhar as equipes com base em fatias que representem funcionalidades de ponta a ponta: cada equipe entrega novas funcionalidades aos seus usuários e, ao mesmo tempo, reduz o volume de coordenação necessário. Em certo sentido, estamos falando de equipes fullstack.
O sucesso de uma equipe é menos determinado pela quantidade de funcionalidades lançadas e mais pela melhoria da experiência das pessoas que utilizam o software.
Quando estamos afastados do usuário final, fica mais difícil saber se nossas contribuições são bem-sucedidas, e corremos o risco de manter o foco em objetivos distantes daquilo que realmente interessa a quem usa o nosso software.
Vamos rever os motivos pelos quais existem equipes dedicadas de frontend — especialistas, consistência e desafios técnicos — e ver como cada uma dessas questões pode ser resolvida.
Compartilhando especialistas
A abordagem tradicional para estruturas organizacionais, que reúne na mesma equipe todas as pessoas com o mesmo conjunto de aptidões, acaba resultando em uma organização em silos, além de impedir que outros desenvolvedores adquiram essas aptidões.
Por exemplo, não é preciso que todos os desenvolvedores se tornem especialistas em iOS, mas seria interessante que alguns adquirissem conhecimento suficiente nessa área para ajudar nas tarefas simples, deixando os especialistas livres para se dedicarem às tarefas realmente complexas.
Criar uma comunidade de prática para compartilhar conhecimentos, desafios, ideias e habilidades pode ser uma abordagem interessante. O objetivo é dar aos especialistas mais espaço e foco nos problemas difíceis, que realmente exijam sua atenção — ou seja, criar um modo mais eficaz de disponibilizá-los.
Outro modelo consiste em manter uma equipe dedicada à capacitação, cujo foco é ajudar as demais equipes a entregar novas funcionalidades, atuando como uma espécie de consultoria interna.
Assim, independentemente de os especialistas estarem alocados em tempo integral em uma equipe ou trabalharem para capacitar outras a fazerem o mesmo trabalho, é possível eliminar os silos organizacionais e, ao mesmo tempo, ajudar os colegas a desenvolverem novas aptidões.
Garantindo a consistência
O segundo motivo citado para a existência de equipes dedicadas de frontend é a consistência: garantir que a UI tenha aparência uniforme, usando as mesmas fontes e cores e resolvendo os mesmos problemas de interface sempre da mesma maneira.
Essa consistência demonstra certo grau de zelo com o próprio produto e torna o software mais fácil de usar.
Há vários meios de garantir certo grau de consistência entre equipes, por exemplo:
- Adotar o modelo de equipes de capacitação, com especialistas passando um tempo em cada equipe.
- Criar recursos compartilhados, como um guia de estilo CSS.
- Disponibilizar uma biblioteca de componentes de UI compartilhados.
Equipes de capacitação são especialmente úteis porque compartilham sua expertise na entrega de componentes prontos e ajudam a garantir que as UIs apresentem uma experiência de usuário consistente.
Algumas empresas tomam a decisão — aparentemente contraintuitiva, mas consistente — de não exigir uniformidade em suas interfaces de usuário, por considerarem preferível dar mais autonomia às equipes. É o caso da AWS: ao menos naquele contexto, a rapidez nas entregas é mais importante do que a consistência na experiência do usuário.
Resolvendo os desafios técnicos
Passamos por evoluções interessantes no desenvolvimento de interfaces de usuário: das interfaces textuais em terminais de tela verde às ricas aplicações desktop, web e apps mobile nativos.
Com frequência, ainda trabalhamos com os mesmos controles de UI de vinte anos atrás — botões, caixas de seleção, formulários e assim por diante. O que mudou foi a tecnologia usada para criar essas interfaces gráficas. A abordagem mais atual são as Single Page Applications (SPAs), que, embora resolvam muitos problemas, dificultam a decomposição de uma interface de usuário. Além disso, a diversidade de dispositivos em que essa UI precisa funcionar traz desafios adicionais.
No fim das contas, queremos que nossos usuários interajam com o software da forma mais natural possível. Seja por meio de um browser em um desktop, seja por um app móvel web ou nativo, o resultado deve ser o mesmo. A forma como a interface foi construída — modular ou monolítica — é o que menos importa para eles.
Frontend monolítico
É a arquitetura na qual todo o estado e o comportamento da UI são definidos na própria UI, com requests feitos aos microsserviços de apoio para obter os dados necessários ou executar as operações exigidas.
Normalmente, os requisitos impostos aos microsserviços são simples: os serviços downstream só precisam expor as informações em um formato que a UI consiga interpretar facilmente — na prática, JSON é o formato mais utilizado.
Entre as desvantagens dessa abordagem estão o incentivo à criação de uma equipe dedicada de frontend, a dificuldade de várias equipes compartilharem a responsabilidade por um mesmo frontend monolítico e a baixa capacidade de tornar a aplicação responsiva às mudanças de negócio.
Esse padrão funciona melhor quando queremos que toda a implementação e o comportamento da UI estejam em um único deploy.
Microfrontends
Também chamado de microsserviço no frontend, é o padrão organizacional segundo o qual é possível trabalhar em diferentes partes de um frontend e implantá-las de modo independente. São pequenas aplicações frontend que, juntas, compõem um todo maior.
A implementação para frontends web pode envolver uma decomposição baseada em widgets, combinando diferentes partes do frontend em uma única tela, ou uma decomposição baseada em páginas, com o frontend dividido em páginas web independentes.
Trata-se de uma arquitetura essencial caso você queira adotar equipes end to end alinhadas a fluxos. Também pode ser útil quando se deseja manter uma estrutura em camadas, mas a funcionalidade do frontend é tão extensa que exige várias equipes dedicadas.
Decomposição baseada em páginas
A UI é decomposta em várias páginas web e, para cada página acessada, os requests são encaminhados ao microsserviço responsável a fim de obter as informações necessárias. Por exemplo, requests para /orders são encaminhados ao microsserviço de orders, enquanto requests para /cashback vão para o microsserviço de cashback.
Do ponto de vista técnico, a simplicidade dessa abordagem é muito atraente: o usuário clica em um link e uma nova página é requisitada.
Decomposição baseada em widgets
A tela é composta por widgets que podem ser alterados de modo independente. Em um e-commerce, por exemplo, uma mesma tela pode conter um widget para o carrinho de compras e outro para as recomendações, cada um com o seu próprio microsserviço responsável. Assim, quando o usuário interage com o carrinho, o microsserviço de carrinho é acionado.
Outro exemplo real dessa implementação é o Spotify, em que um widget exibe a playlist, outro traz informações do artista, e assim por diante.
Um ponto de atenção é a necessidade de algum tipo de camada de montagem para reunir as partes. Ela pode ser algo simples, como o uso de templates. O ideal é que cada widget seja empacotado como um todo, não provoque falhas em outras áreas da UI e não gere conflitos com os demais.
A grande vantagem dessa abordagem é permitir, por exemplo, que o carrinho de compras use React v16 enquanto as recomendações continuam em React v15. Podemos, portanto, usar diferentes versões de framework em widgets distintos, o que facilita as atualizações. Fazer o upgrade de uma UI monolítica inteira pode ser assustador; fazê-lo de forma gradual reduz o risco de introduzir novos problemas.
Muitos widgets tendem a gerar duplicação entre suas dependências, o que aumenta o tamanho total da página a ser carregada.
Mesmo sendo implantados de modo independente, queremos que os widgets sejam capazes de interagir entre si. Uma forma de implementar isso é fazer com que um widget emita eventos personalizados que serão "escutados" pelos demais.
O uso desse padrão facilita a contribuição de várias equipes para a mesma UI e traz mais flexibilidade, já que widgets lançados por equipes diferentes podem coexistir na mesma interface.
Gateway de agregação central
O gateway de agregação fica entre as interfaces de usuário externas e os microsserviços downstream, realizando a filtragem e a agregação de chamadas. Sem ele, a interface de usuário teria de fazer várias chamadas para reunir as informações de que precisa.
Com o gateway, a interface faz uma única chamada. O gateway dispara todas as chamadas necessárias, combina os resultados em uma única resposta e descarta os dados que não interessam àquela interface. Ele também pode fazer chamadas em batch, recebendo a lista de ids que queremos consultar e cuidando do restante.
Dessa forma, reduzimos a quantidade de requests que o cliente externo precisa fazer, diminuindo o consumo de banda e melhorando a latência percebida.
O principal ponto de atenção é a responsabilidade sobre o gateway, pois ele tem grande potencial de se tornar um gargalo na entrega de software:
- Se várias equipes precisarem alterá-lo, o desenvolvimento exigirá coordenação constante entre todas elas, tornando o trabalho mais lento.
- Se uma única equipe for responsável por ele, essa equipe pode se tornar o gargalo.
Resolvida a questão da responsabilidade, resta outro ponto: os diferentes dispositivos têm necessidades diferentes. A natureza das interações que queremos oferecer em um dispositivo móvel pode ser completamente distinta da esperada em uma aplicação desktop.
Na prática, queremos que cada dispositivo faça o menor número possível de requisições, obtendo somente os dados necessários para exibição. Isso implica adicionar, na API do backend ou no próprio gateway, o suporte a diferentes tipos de interface de usuário — exatamente o que faz o gateway crescer em complexidade.
Ao lidar com chamadas de API, surgem ainda outras preocupações mais genéricas, como o gerenciamento de chaves de API, a autenticação de usuários e o roteamento de chamadas. Normalmente, essas responsabilidades são tratadas por produtos específicos, os API gateways.
A depender do nível de necessidade, pode fazer sentido comprar um produto de mercado. Contudo, é preciso cuidado ao usá-lo para agregação e filtragem de chamadas: ao adotar estruturas específicas do vendor, boa parte da inteligência do sistema acaba depositada em um produto de terceiros. A situação piora se o gateway de agregação ficar tão complexo a ponto de exigir uma equipe dedicada apenas para desenvolvê-lo e administrá-lo.
Para uma solução em que uma única equipe desenvolve a UI e os microsserviços de backend, um gateway de agregação central pode fazer sentido. Já quando várias equipes o utilizam, ele tende a exigir um volume elevado de coordenação.
Backend for frontend (BFF)
A principal diferença entre um gateway de agregação e um BFF é que o BFF tem um propósito único: ele é desenvolvido para uma interface de usuário específica. Esse padrão tem se mostrado muito bem-sucedido para lidar com os diferentes problemas relacionados às interfaces de usuário — as interfaces web e mobile passam a ter, cada uma, o seu próprio backend de agregação.
Considerando sua natureza específica, o BFF elimina alguns dos problemas do gateway de agregação central. Como ele atende a uma única interface, tem menos chances de se tornar um gargalo para o desenvolvimento; e, supondo que a mesma equipe seja responsável pelo BFF e pela UI, o acoplamento inerente entre eles se torna muito mais fácil de administrar.
Normalmente, a abordagem mais interessante é ter estritamente um BFF para cada tipo de cliente — inclusive no mobile, em que Android e iOS ganham BFFs separados, ainda que ofereçam funcionalidades semelhantes. Uma variação possível é usar o mesmo BFF para mais de um tipo de cliente, mas é preciso cautela: quanto mais tipos de clientes compartilharem um único BFF, mais inflado ele ficará por ter de lidar com preocupações distintas.
Duplicação entre BFFs
Uma das preocupações de manter um BFF por interface é acabarmos com muita duplicação entre eles: BFFs diferentes podem fazer os mesmos tipos de agregação e ter código idêntico ou semelhante para se comunicar com os serviços downstream. Algumas pessoas reagem a isso tentando combinar os BFFs novamente e acabam recriando um gateway de agregação de propósito geral.
Dentro de um único microsserviço, refatoramos e eliminamos a duplicação naturalmente. Entre microsserviços, porém, extrair código compartilhado pode gerar um acoplamento alto — o que costuma ser pior do que a duplicação que se pretendia eliminar.
Quando realmente houver necessidade de reaproveitar código comum entre os BFFs, há duas opções:
| Opção | Custo | Risco principal | Quando faz sentido |
|---|---|---|---|
| Biblioteca compartilhada | Baixo | Fonte significativa de acoplamento, sobretudo ao gerar client libraries para chamadas a serviços downstream | Código puramente técnico e estável, com deploy independente por consumidor |
| Novo microsserviço | Alto | Mais um serviço para operar | A funcionalidade extraída representa um conceito do domínio de negócio |
Criar uma abstração apenas quando estiver prestes a implementar algo pela terceira vez costuma ser uma regra razoável.
Em uma aplicação que disponibilize somente uma UI web, o BFF faz sentido apenas se houver um volume significativo de agregações do lado do servidor. Já no momento em que for preciso oferecer funcionalidades específicas para uma UI móvel ou para terceiros, vale considerar o uso de um BFF por cliente desde o princípio — a menos que o custo de implantar serviços adicionais seja alto, embora a separação de responsabilidades proporcionada pelo BFF seja bastante atraente na maioria dos casos.
GraphQL
O GraphQL é uma linguagem de consulta que permite aos clientes executar queries para acessar ou modificar dados. As queries podem ser alteradas dinamicamente, possibilitando que o cliente defina exatamente quais informações quer obter — apenas os campos necessários —, o que reduz os acessos de ida e volta ao servidor.
Além disso, temos a facilidade de alterar uma agregação modificando somente a query do cliente, sem exigir mudanças no lado do servidor.
query {
pedido(id: "123") {
total
cliente {
nome
}
itens {
descricao
quantidade
}
}
}
Para atender às queries, precisamos de um resolver, responsável por buscar os dados de cada campo. Para alterar o estado do servidor, utilizamos as mutations.
O GraphQL acaba sendo uma forma simples de implementar um BFF, já que a própria linguagem de consulta cuida da agregação e da filtragem dos dados que a interface precisa.
Conclusão
Repensar a interface de usuário como parte integrante da entrega — e não como uma camada separada — é o que sustenta a adoção de equipes alinhadas a fluxos. Os três motivos que costumam justificar equipes dedicadas de frontend têm alternativas concretas: comunidades de prática e equipes de capacitação para compartilhar especialistas, guias de estilo e bibliotecas de componentes para garantir consistência, e microfrontends para vencer os desafios técnicos da decomposição.
No lado da agregação, a escolha do padrão está diretamente ligada à estrutura das equipes:
| Padrão | Melhor cenário | Principal risco |
|---|---|---|
| Frontend monolítico | UI entregue em um único deploy, por uma só equipe | Incentiva silos e reduz a autonomia |
| Microfrontends (páginas) | Fatias de funcionalidade bem separadas por rota | Navegação menos fluida entre páginas |
| Microfrontends (widgets) | Várias equipes contribuindo para a mesma tela | Duplicação de dependências e páginas maiores |
| Gateway de agregação central | Uma equipe responsável pela UI e pelos serviços | Vira gargalo quando muitas equipes dependem dele |
| BFF | Múltiplos tipos de cliente com necessidades distintas | Duplicação entre os BFFs |
| GraphQL | Clientes que precisam definir dinamicamente os dados | Complexidade nos resolvers e no controle de carga |
Escolha o padrão de agregação que minimize a coordenação entre equipes. Se um componente da arquitetura exige que várias equipes se sincronizem para entregar uma funcionalidade, ele já é um gargalo — independentemente de quão elegante seja tecnicamente.
No fim, o usuário não percebe se a interface é modular ou monolítica: ele percebe a rapidez com que as melhorias chegam até ele e a qualidade da experiência que recebe. Toda decisão arquitetural no frontend deveria ser avaliada por esse critério.
