Barte/ dev

Arquitetura

Uma rota por vez: como migramos 26 telas sem parar o produto

Uma estratégia simples, baseada em rotas, feature flags e observabilidade, para substituir gradualmente uma aplicação no-code por React.

por Nagel··9 min de leitura

Problemas complexos nem sempre exigem soluções complexas.

Quando decidimos substituir nossa aplicação no-code por React, poderíamos ter criado um grande projeto de reescrita, congelado o desenvolvimento de novas funcionalidades e trabalhado durante meses antes de colocar qualquer coisa em produção.

Escolhemos outro caminho: migrar uma rota por vez.

A estratégia não era disruptiva nem particularmente sofisticada. Ela combinava alguns padrões conhecidos, componentes que já utilizávamos e uma regra de ouro para evitar que o legado continuasse crescendo.

O resultado foi uma migração gradual de 26 telas, organizadas em 12 fluxos principais, enquanto o produto continuava evoluindo.

Quando o atalho começa a cobrar manutenção

No início de uma empresa, velocidade é uma vantagem competitiva.

POCs, MVPs e ferramentas no-code permitem validar ideias rapidamente, aprender com os primeiros usuários e construir experiências completas sem o custo inicial de uma estrutura tradicional.

Na Barte, uma aplicação no-code cumpriu bem esse papel. Ela permitiu construir o painel usado pelos nossos usuários, integrar serviços e adaptar o produto conforme entendíamos melhor o mercado.

O problema apareceu com o crescimento.

A manutenção passou a depender de poucas pessoas. Sem padrões consistentes, algumas partes se tornaram difíceis de entender e alterar. Uma mudança em um ponto podia gerar efeitos inesperados em outro, enquanto a cobertura por testes automatizados era limitada e difícil de expandir.

O tempo de carregamento também aumentou, e determinadas integrações exigiam soluções muito específicas da plataforma. Aos poucos, a velocidade que justificou a escolha inicial começou a ser perdida na manutenção.

Migrar para React passou a ser menos uma decisão de tecnologia e mais uma decisão de autonomia. É o mesmo raciocínio que virou princípio explícito na engenharia da Barte: nada de no-code/low-code no core.

O desafio não era reescrever as telas

Visualmente, muitas páginas pareciam simples. O verdadeiro problema estava nas regras de negócio acumuladas ao longo dos anos.

Parte delas estava documentada. Outra parte existia apenas em condicionais, transformações de dados e comportamentos da interface. Em alguns casos, não havia mais ninguém com todo o contexto necessário para explicar por que determinada regra existia.

Migrar significava reconstruir esse conhecimento sem alterar o resultado percebido pelo usuário.

Também não poderíamos desenvolver a nova aplicação inteira antes de colocá-la em produção. Além de retardar o aprendizado, isso criaria um grande momento de virada no qual todos os riscos apareceriam ao mesmo tempo.

Precisávamos manter as duas experiências funcionando em paralelo, migrar gradualmente e voltar rapidamente caso algo desse errado.

Primeiro, uma POC de algumas horas

A estratégia seguia um padrão conhecido como Strangler Fig Pattern: em vez de substituir toda a aplicação de uma vez, novas partes seriam construídas ao redor do legado até que ele pudesse ser completamente desligado.

Para validar essa arquitetura, construímos uma POC com duas origens atrás do CloudFront: a aplicação no-code existente e a nova aplicação React.

No CloudFront, cada Behavior associa um padrão de rota a uma origem. O Behavior padrão continuava direcionando as requisições para o legado, enquanto Behaviors mais específicos encaminhavam determinados caminhos para o React. Essa técnica, conhecida como path-based routing, permitia migrar uma família de páginas sem interferir no restante da aplicação.

A anatomia de uma rota migrada

Vale abrir essa configuração, porque ela é a unidade que acabamos repetindo a cada fluxo.

A aplicação React não era uma origem qualquer. Ela é uma distribuição do CloudFront, registrada como origem da distribuição principal. O domínio público continuou apontando para a distribuição de sempre, e foi ela que passou a delegar prefixos de rota para a distribuição do React.

flowchart TD
  U["usuário"] --> D1["distribuição principal<br/>(o domínio público)"]
  D1 -->|"Default (*)"| LEG["aplicação no-code"]
  D1 -->|"/assets/* · /payfac · /payfac/*"| D2["distribuição do React"]
  D2 --> B["bucket com o build<br/>da aplicação React"]

Isso mantinha as duas aplicações independentes. A do React seguia com o próprio pipeline de deploy, o próprio cache e a própria origem, enquanto a principal só precisava saber para onde mandar cada caminho.

Colocar uma rota no ar era criar Behaviors na distribuição principal, respeitando a ordem de precedência:

0 /assets/* → distribuição do React (uma vez: o bundle do SPA)
1 /payfac → distribuição do React ┐ um par
2 /payfac/* → distribuição do React ┘ por rota
...
9 Default (*) → aplicação no-code

Dois detalhes dessa configuração custaram algum tempo até ficarem óbvios.

O primeiro é o par de Behaviors por rota. /payfac/* não casa com /payfac puro, porque a barra é literal. Sem os dois, a URL de entrada sem barra final caía no Behavior padrão e ia parar no legado. Um /payfac* resolveria com um Behavior só, mas casaria também com qualquer caminho que começasse com essas letras.

O segundo é o cache desligado em todos eles, e esse nós aprendemos da pior forma. Quem faz cache é a distribuição de baixo. Com cache também na de cima, o mesmo objeto passa a existir em duas camadas com TTLs independentes: um deploy novo invalidava a distribuição do React, mas a principal continuava entregando a versão anterior até o próprio TTL expirar.

O resultado é o tipo de bug mais ingrato de depurar. Usuários em versões diferentes da mesma tela, sem nada de errado no deploy, e uma invalidação que parecia não surtir efeito porque estava sendo feita na distribuição errada. Cache em camadas só ajuda quando cada camada tem um dono claro.

Faltava um detalhe para o SPA funcionar. Um deep link como /payfac/rates não existe como arquivo no bucket, e um F5 nessa rota devolvia 404. Uma CloudFront Function resolve isso na borda, entregando o index para qualquer caminho que não pareça um arquivo:

function handler(event) {
var request = event.request;
var uri = request.uri;
// deep link do SPA: sem extensão no último segmento, entrega o index
// e deixa o roteador do React resolver no cliente
if (uri.startsWith('/payfac') && !uri.split('/').pop().includes('.')) {
request.uri = '/index.html';
}
return request;
}

O CloudFront determinava qual aplicação receberia a requisição. Depois disso, as próprias aplicações consultavam uma feature flag para decidir se aquele usuário participaria da nova experiência.

Se a flag estivesse desabilitada, o React redirecionava o usuário para a rota correspondente no legado. A aplicação anterior fazia o caminho inverso: ao encontrar uma funcionalidade habilitada, encaminhava a navegação para o React.

As duas aplicações respondiam exatamente pelo mesmo domínio e pelo mesmo subdomínio. Essa escolha permitiu que a nova aplicação reaproveitasse a sessão de autenticação já estabelecida pela experiência anterior.

Para o usuário, a transição era transparente: ele podia navegar entre páginas atendidas por tecnologias diferentes sem precisar se autenticar novamente e sem perceber a troca entre as aplicações.

Em algumas horas, a POC validou as premissas mais arriscadas:

  • roteamento entre as aplicações;
  • reaproveitamento da autenticação;
  • navegação transparente;
  • rollback apenas desligando uma flag.

Começamos pela tela de taxas

Depois da POC, escolhemos o cadastro de taxas como primeira experiência real.

Não era a tela mais fácil. Pelo contrário: era uma das mais complexas e uma das que mais geravam problemas no legado.

O cadastro precisava representar diferentes meios de pagamento, canais, prazos, limites e formas de recebimento. Cartões podiam ter regras diferentes por número de parcelas, chegando a 24 configurações. Também existiam variações por plano, contexto da empresa, MCC e versão da estrutura de taxas.

Escolher essa tela foi importante porque colocou a arquitetura à prova em condições reais. Uma página simples poderia validar o roteamento, mas não mostraria se conseguiríamos extrair regras de negócio, criar testes e sustentar a nova estrutura.

Foi também nesse momento que definimos como a aplicação React seria organizada. Criamos uma estrutura modular, na qual cada área do produto poderia evoluir de forma independente dentro da mesma aplicação.

Durante a reconstrução, não nos limitamos a reproduzir a interface anterior. Mantivemos o comportamento necessário, mas aproveitamos para corrigir débitos de experiência, melhorar filtros e construir os componentes sobre o design system da Barte.

A migração deixou de ser uma simples tradução de tecnologia. Ela passou a ser uma oportunidade de devolver consistência ao produto.

Nossa regra de ouro

Durante todo o projeto, seguimos uma regra simples:

Nada novo seria construído no legado.

Erros críticos continuavam sendo corrigidos imediatamente. Não deixaríamos um usuário com problemas apenas porque aquela experiência seria substituída no futuro.

Mas novas funcionalidades eram desenvolvidas diretamente em React.

Isso também significava que a migração não interromperia o roadmap. Pedidos de novas funcionalidades, melhorias solicitadas pelos usuários e outras prioridades do produto continuaram chegando normalmente.

Em alguns casos, uma nova demanda antecipava a migração de determinada experiência. Em vez de ampliar o legado para depois reescrevê-lo, trazíamos aquele fluxo para React e construíamos a novidade já na estrutura definitiva.

A regra impediu que o alvo continuasse se afastando enquanto tentávamos alcançá-lo.

Uma estratégia simples e repetível

Depois da primeira tela, o processo passou a seguir praticamente o mesmo ciclo:

flowchart TD
  A[escolher uma experiência] --> B[extrair suas regras]
  B --> C[construir em React]
  C --> D[direcionar a rota]
  D --> E[liberar por feature flag]
  E --> F[observar]
  F --> G[ampliar o rollout]
  G --> H[remover o legado]
  H --> A

Cada tela possuía um critério de sucesso baseado em sua ação principal. Em uma página de cadastro, por exemplo, não bastava saber que ela carregou: precisávamos acompanhar se o usuário iniciou, avançou e concluiu a operação.

Instrumentamos esses pontos no PostHog e criamos dashboards específicos para acompanhar cada rollout. Isso permitia detectar abandonos, erros e comportamentos inesperados antes que o problema se transformasse em um volume maior de chamados.

Os componentes migrados também receberam testes unitários, especialmente nas áreas em que as regras de negócio eram mais densas. Os testes não substituíam a observação em produção, mas reduziam o risco de repetir o comportamento em um cenário e quebrá-lo em outro.

A liberação seguia uma estratégia de progressive delivery. Começávamos com um grupo selecionado de usuários, avançávamos para aproximadamente 10% da base e só então seguíamos para o rollout completo.

Quando necessário, o rollback era apenas uma alteração na feature flag. Com a flag desabilitada, a rota voltava para a experiência anterior sem exigir um novo deploy.

Ao chegar a 100%, removíamos a implementação antiga e mantínhamos o redirecionamento para a nova aplicação. A cada ciclo, o legado ficava um pouco menor.

Repetimos esse processo até completar a migração. Como o mesmo mecanismo era aplicado a cada experiência, não houve uma grande virada nem um momento em que toda a base precisou assumir o risco de uma aplicação completamente nova.

Houve apenas uma sequência de pequenas mudanças reversíveis.

O simples pode funcionar

O principal aprendizado dessa migração não foi sobre React ou CloudFront.

Foi sobre reduzir o tamanho das decisões.

Não criamos uma plataforma sofisticada de migração. Usamos duas origens, regras de rota, feature flags, testes e observabilidade. Cada elemento tinha uma responsabilidade simples e conhecida.

O Strangler Fig Pattern deu direção à arquitetura. O path-based routing separou as experiências. As feature flags controlaram a exposição da nova aplicação. O progressive delivery limitou o impacto dos erros. A regra de ouro impediu que o legado continuasse crescendo.

Separadamente, nenhuma dessas ideias é nova. O valor estava em combiná-las de uma forma que o time conseguisse repetir.

Às vezes, a melhor estratégia para substituir um sistema complexo não é desenhar uma solução igualmente complexa.

É migrar uma rota, observar e repetir.