Apps e BFF - Solucionando problemas com uma nova arquitetura

Nós, desenvolvedores e arquitetos de software, tendemos a evitar “reinventar a roda” e, sempre que possível, usar o que é o padrão de mercado para desenvolver qualquer tipo de produto. O desafio, nesse caso, é saber quando é benéfico sair um pouco desse padrão. Às vezes precisamos pensar fora da caixa para conseguir trazer benefícios para a produtividade do time de desenvolvimento e, consequentemente, mais valor a um produto. Podemos resumir essa ideia ao pensamento de que desenvolver um produto fora de série exige soluções criativas.

Aqui no Elo7 temos a cultura de criar arquiteturas baseadas no conceito “Product Driven”. Isso significa que o produto que queremos entregar é o fator que mais influencia na arquitetura das nossas aplicações.

O que queremos apresentar neste post é como nós do Elo7, a partir de diversos desafios que enfrentamos, desenvolvemos uma arquitetura para nossos apps pensada em resolvê-los.

Para deixar mais claro, vamos citar alguns dos requisitos que levaram ao desenvolvimento desta arquitetura e detalhá-los durante este post. São eles:

  • Deep links devem abrir o aplicativo exibindo o mesmo conteúdo do site;
  • Deep links com parâmetros de filtro ou tracking (UTM) devem ser interpretados em telas nativas;
  • Todo conteúdo das telas, como texto e imagens, devem ser dinâmicos. Não podemos depender de um release quando precisarmos modificá-los;
  • Ao criar uma versão nativa para uma página inicialmente implementada com WebView, devemos fazer testes A/B para analisar possíveis impactos em conversão.

Antes de mais nada, caso não tenha familiaridade sobre deep links, a documentação do Android explica com detalhes.

Em todos os apps de Marketplace, é seguro afirmar que o ponto de entrada do aplicativo pelo menu do celular é só um dos muitos possíveis. Caso a pessoa toque num link através de uma rede social ou buscador, link que pode ser gerado por tráfego pago ou orgânico, caso nosso app esteja instalado precisamos abri-lo mostrando o mesmo conteúdo que a pessoa veria na página web, porém oferecendo uma experiência mobile nativa.

Existem várias formas de tratar esse ponto de entrada por links, mas como fazer isso da melhor forma possível, onde o app consiga responder dinamicamente qualquer nova página do site sem precisar de um novo release? Como saber se temos uma tela já desenvolvida para a funcionalidade que o link leva? Caso esse link tenha parâmetros de filtro, como fazer que eles tenham efeito? E parâmetros de UTM, como tratar?

Na primeira implementação feita nos apps, toda a lógica responsável por identificar a URL de entrada e montar uma tela nativa com os dados presentes nela estava no front.

Fluxo:

fluxo de abertura de deeplink legado

O fluxo acima funciona bem, mas traz alguns problemas: duplicação de código nos apps Android e iOS, possíveis bugs dependem de um release do app para serem corrigidos e, por fim, parâmetros de UTM tinham que ser convertidos em cookies e persistidos manualmente.

A solução

Para a solução de quase todos os problemas que discutiremos aqui, falaremos dele: o nosso BFF (backend for frontend).

A grande sacada de um BFF é fazer com que o backend dos aplicativos proveja dados de uma forma muito mais organizada e “mastigada” para o app consumir. No nosso caso, fomos além de construir um BFF pensando só nos contratos de resposta, nós o projetamos para que também pudéssemos ter contratos de requisição otimizados para o nosso produto. Com isso, veio a ideia de fazer a chamada ao backend com a mesma URL que recebemos por deep links, e, a partir daí, toda a lógica ficar como responsabilidade do servidor.

O novo fluxo com o BFF ficou da seguinte maneira:

fluxo de abertura de deeplink com bff

Com toda a lógica no servidor, o código dos apps fica mais limpo e as correções de possíveis bugs podem ser colocadas em produção sem necessidade de lançar um nova versão do app. Além disso, a implementação no BFF da conversão dos parâmetros de UTM, resultando no header de resposta set-cookie, faz com que no Android precisemos apenas garantir que o cliente HTTP salve esse dado no gerenciador de cookies. No iOS, utilizando libs como o Alamofire, isso já é feito de forma automática.

No último diagrama, talvez você tenha percebido que tiramos o passo “Faz match da URL para tela nativa”. Com a nova arquitetura, abandonamos a ideia de ter o que chamávamos de Path Resolver: uma classe que executava um loop testando a URL recebida, checando se havia uma tela nativa para ela. Passamos a pensar toda a navegação do app otimizada para links, dessa maneira, todo o path de URL passou a ser associado a uma Activity ou ViewController.

E como fizemos essa associação? Pretendo fazem em breve um outro post detalhando essa solução =)

O problema das WebViews

Existe um atalho muito comum que podemos tomar para sair do zero ao app nativo funcional economizando tempo e custo. WebViews possibilitam introduzir no aplicativo features que já estavam prontas para a web com pouco esforço de desenvolvimento, o que faz com que possamos gerar muito valor em pouco tempo. Sabemos porém que, apesar dessa grande vantagem, features implementadas com WebView tendem a ter problemas de performance, usabilidade, acessibilidade e falta de controle pelos desenvolvedores do aplicativo.

Lá no longínquo ano de 2015, quando lançamos a primeira versão do App iOS para compradores, escrevemos sobre essa arquitetura híbrida.

Vale lembrar que, nessa época, os frameworks cross platform ainda estavam engatinhando.

Aqui no Elo7, estamos continuamente reescrevendo features originalmente implementadas com WebView em fluxos nativos. Fazer isso a princípio parece ser algo simples na visão de “como fazer”. O simples “é só” reescrever a funcionalidade e trocar os links que a acionam, porém, quando estamos trabalhando num app com milhares de usuários ativos, onde qualquer alteração pode impactar negativamente em conversão e receita, refazer qualquer coisa é uma tarefa que demanda muito cuidado.

A primeira solução, que pensamos quando estávamos discutindo como fazer um teste A/B entre uma nova tela nativa e sua versão legada WebView, foi fazer a alteração do fluxo via configuração remota usando o Firebase, o que é uma solução tecnicamente boa e viável e já utilizamos para testar mudanças de layout em telas já nativas. Porém, com a possibilidade de usarmos um BFF, optamos por deixar essa responsabilidade com ele. A vantagem é que conseguimos criar uma solução padronizada, onde toda a nova tela criada no aplicativo pode ter uma irmã WebView, e a decisão de qual mostrar é controlada no backend.

Diagrama:

fluxo de abertura de tela webview ou nativa

A ideia é que toda nova Activity ou ViewController dos aplicativos, assim como uma página web, sejam associados a um path de URL e também herdem de uma classe base. Essa classe contém a implementação do que mostrar quando o BFF responde JSON ou HTML.

Para deixar a ideia da classe base mais clara, vamos detalhar com código Android Java:

class BaseActicity<T> {

	@Override
	protected void onCreate(@Nullable Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
	
		viewModel.getActivityStateTypeLiveData().observe(this, activityState -> {
	
			switch (activityState.getActivityStateType()) {
				case NATIVE:
					onNativeResponse(activityState.getNativeData());
					break;
				case WEB_VIEW:
					buildWebViewWith(uri.toString());
					break;
	
			}
		});
	}
	
	private void buildWebViewWith(String uri) {
		// constrói webview
	}

	abstract void onNativeResponse(T nativeData);
}

Explicando: com a classe base, toda tela que a herde ganha a capacidade de construir uma WebView com o conteúdo retornado. No caso de uma resposta JSON, a classe filha terá que implementar o método abstrato onNativeResponse, onde ficará o código de visualização nativo.

Conteúdo em responsabilidade do backend

Com um backend exclusivo para uso dos nossos apps, ganhamos liberdade total na hora de pensarmos em um contrato de resposta.

Uma API REST padrão, em geral, retorna dados crus para que cada sistema que a consuma monte os dados da maneira que bem entender. Com um BFF, podemos colocar na resposta, por exemplo, informações de moeda e números já formatadas em texto, além de representarmos na resposta cada componente de uma tela, já com todo seu conteúdo textual.

Exemplo de resposta de um endpoint REST de busca de produtos:

https://api.elo7.com.br/search?query=amigurumi

{
	"result": [
		{
			"name": "boneco amigurumi",
			"price": "50.9",
			"installment_number": "12",
			"installment_value": "4.24",
			"installment_free": true
		}
	]
}

Exemplo de resposta do BFF:

https://www.elo7.com.br/lista/amigurumi

{
	"header": {
		"results": "1000 produtos encontrados",
		"share": {
			"message": "Gostei destes produtos do Elo7: https://www.elo7.com.br/lista/amigurumi"
		}
	},
	"searchBar": {
		"hint": "Buscar",
		"text": "amigurumi",
	},
	"productList": [
		{
			"name": "boneco amigurumi",
			"price": {
				"value": "R$ 50,90",
				"installment": "12x de R$ 4,24 sem juros",
			}
		}
	]
}

Com esse exemplo, fica claro que, se estivéssemos consumindo uma API REST, teríamos que duplicar código nativo no Android e no iOS para converter valores, além de eventualmente precisar consumir diversos endpoints para obter tudo que precisamos na tela. Com o BFF, podemos delegar a ele a responsabilidade de consumir n endpoints e também fazer com que cada objeto de resposta represente um componente da tela e já dê o seu valor formatado.

Outra vantagem clara é o exemplo do objeto header apresentado no JSON acima. Ele já dá a quantidade de produtos encontrados em texto e a mensagem que usamos no momento em que a pessoa usa a feature “compartilhar”. No caso de precisarmos trocar algo nesse texto, todas as pessoas usuárias do app já veriam os dados atualizados.

Desvantagens da arquitetura

Um fato que todos nós sabemos: não existe solução perfeita. Nós consideramos que conseguimos criar uma solução boa o bastante para nossos principais problemas, mas já sabemos que teremos que conviver com algumas limitações e ter alguns cuidados ao longo do tempo.

Maior processamento no servidor

Um dos principais receios de colocar mais responsabilidade no backend é deixar as respostas mais lentas, o que é uma preocupação válida. É muito importante para quem considera construir um BFF garantir uma infraestrutura compatível com a carga que pretende receber. Para isso, testes de carga são muito importantes. O que podemos adiantar com a experiência que tivemos aqui no Elo7, para nossa demanda e tipo de produto, é que é sim viável ter um BFF com as responsabilidades que descrevemos aqui.

Colocamos a ideia de ter telas associadas a links como uma vantagem, mas isso também traz algumas limitações. É comum pensarmos fluxos exclusivos para o app ou telas com comportamentos diferentes entre app e web. Quando temos que lidar com essas diferenças, temos que discutir formas abrir exceções no contrato padrão.

Também é importante reforçar que essa implementação resolve problemas que tínhamos nos nossos aplicativos do Elo7, e que não necessariamente são problemas para outros tipos de apps. Talvez para um produto em que o aplicativo seja a principal solução, essa ideia não seja tão interessante.

Conclusão

Construir uma arquitetura que solucionasse nossos problemas da melhor forma possível levou bastante tempo. Foram diversas conversas até chegar em um modelo que fosse bom o suficiente.

Apesar de todas as vantagens que ela traz, já mapeamos pontos de atenção e precisamos estar atentos a possíveis problemas que ela pode trazer, sem receio de admitir pontos negativos e parar para fazer correções e melhorias.

O que achou desta solução que apresentamos? Deixe um comentário ou dúvida que estaremos dispostos a responder =)