Como reorganizamos o checkout de um grande e-commerce sem parar de vender
Sumário
- O que é o Atlas, e por que ele precisava crescer
- A dor, sem romantizar
- Primeira decisão: cada domínio é um sub-app
- Segunda decisão: dar um cérebro, um corpo e uma memória pra cada tela
- A jogada que faz a diferença: parar de pedir, começar a impedir
- A fuga das dependências instáveis
- O que tudo isso custou
- Os números, pra não ficar no “achismo”
- O teste de verdade: o carrinho chegando
- O que eu levaria pra qualquer time
Tem um tipo de código que ninguém escreve de propósito. Ele simplesmente acontece.
Quando abri o arquivo da página de pagamento do nosso checkout pela primeira vez, ele tinha quase 200 linhas. Numa olhada rápida, dava pra ver de tudo ali dentro: uma chamada de API buscando os dados do pedido, três acessos diretos a stores de estado, um if decidindo pra qual tela navegar, e no meio disso um disparo de evento de analytics. Um arquivo só, fazendo o trabalho de quatro.
Ninguém sentou e decidiu “vou misturar tudo aqui”. Foi a soma de dezenas de pequenas decisões razoáveis, cada uma tomada sob prazo, cada uma “só mais essa coisinha aqui”. O resultado é o que a gente chama, sem carinho nenhum, de bola de lama.
E o detalhe que torna essa história interessante: isso não era um projetinho de fim de semana. Era o checkout de um dos maiores e-commerces de pet do Brasil. Dinheiro de verdade passando por ali, o tempo todo. A gente precisava arrumar a casa sem nunca apagar a luz.
Esse artigo é sobre como fizemos isso. Vou chamar o sistema de Atlas pra não citar nomes internos, mas tudo que descrevo aconteceu de verdade, em produção, com gente comprando ração enquanto a gente trocava o motor do avião.
O que é o Atlas, e por que ele precisava crescer
O Atlas é o checkout. Aquele pedacinho final e mais delicado de qualquer e-commerce: a pessoa já decidiu comprar, colocou as coisas no carrinho, e agora precisa passar pelo funil de dados, endereço, entrega e pagamento até a compra se concretizar.
É um microfrontend. Ou seja, não é o site inteiro — é uma aplicação separada, responsável só por esse fluxo, que conversa com a plataforma principal (vou chamar de “o monólito”) e com um monte de outros serviços: meios de pagamento, programa de fidelidade, clube de descontos, cálculo de frete.
Tecnicamente, é uma aplicação Nuxt 4 com Vue 3, estado em Pinia, comunicação via GraphQL. Se você não é dev, não precisa guardar esses nomes — são só as ferramentas. O que importa é o seguinte: toda página do Atlas exige usuário logado e um pedido válido, e cada passo conversa com o backend pra avançar o funil. É um fluxo com estado, sequencial e crítico. Errar aqui não é um bug bonitinho na home — é uma venda que não acontece.
E tinha mais uma coisa no horizonte. Até então o Atlas cuidava só do checkout. Mas a decisão de produto já estava tomada: o carrinho também viria pra cá. Ou seja, o Atlas ia deixar de ser “a aplicação de checkout” pra virar “a aplicação de checkout e carrinho” — e, possivelmente, de outros domínios depois.
Isso muda tudo. Uma coisa é organizar uma casa onde mora uma família. Outra é organizar uma casa sabendo que daqui a seis meses ela precisa virar um prédio. A pergunta deixou de ser “como deixo esse código mais limpo?” e virou “como preparo esse código pra receber vizinhos sem que todo mundo se atropele?”.
A dor, sem romantizar
Antes de falar de solução, vale ser honesto sobre o tamanho do problema. Não era “ah, o código podia estar mais bonito”. Eram dores concretas, que apareciam toda semana.
Tudo morava na raiz. O Atlas nasceu com a estrutura padrão do Nuxt: páginas, componentes e lógica jogados juntos na raiz do projeto. Funciona lindamente quando você tem um domínio pequeno. Vira um pesadelo quando você precisa colocar um segundo domínio do lado e nada separa o que é de quem.
As camadas estavam grudadas umas nas outras. As páginas importavam as stores de estado diretamente. Parece inofensivo, mas cria uma dependência invisível: o dia em que alguém mexe na estrutura interna de uma store, esse efeito vaza para todas as páginas que a usam — sem nenhum aviso, sem nenhuma barreira. Você só descobre quando algo quebra.
A regra de negócio tinha vazado pra dentro das stores. Esse foi o sintoma mais claro de que as fronteiras tinham se perdido. Uma store, que deveria guardar apenas estado, tinha ganhado uma função que chamava a API. Outra tinha uma função que decidia navegação e recarregava a página. Pare um segundo nisso: o lugar responsável por “guardar o que está na tela” tinha aprendido a “ir buscar dados” e a “trocar de tela”. São responsabilidades completamente diferentes, no lugar errado.
Quase nada era testável de forma decente. Quando uma página junta busca de dados, estado, navegação e analytics num arquivo só, não dá pra testar uma regra isolada. Pra verificar algo simples como “quando não houver endereço cadastrado, mande o usuário pra tela de cadastro”, você era obrigado a montar a página inteira e simular meia dúzia de efeitos colaterais. Teste que dá trabalho demais é teste que não se escreve.
E o pior de tudo: nada impedia a repetição. A gente tinha documentação dizendo “não faça assim”. Mas documentação é um pedido. No dia da pressa, o próximo dev — talvez eu mesmo — ia abrir uma página, copiar o padrão antigo que estava ali do lado e seguir em frente. A bagunça não era um estado; era uma tendência. Ela se reproduzia sozinha.
Esse último ponto é o que mais importa pra história. Porque ele mudou a natureza da solução que a gente foi atrás. Não bastava limpar. A gente precisava de algo que impedisse a sujeira de voltar.
Primeira decisão: cada domínio é um sub-app
A primeira virada foi parar de pensar em “pastas” e começar a pensar em “domínios”.
O Nuxt tem um recurso chamado layers. A ideia é simples de explicar com uma analogia: em vez de um apartamento grande onde tudo divide os mesmos cômodos, você constrói um prédio onde cada andar é uma unidade completa e autossuficiente. Cada layer tem suas próprias páginas, seus componentes, seu estado, seus testes. A raiz do projeto vira só o porteiro: ela monta o prédio e decide a ordem dos andares, mas não guarda mobília de ninguém.
A gente quebrou o Atlas em duas layers logo de cara:
- shared — a infraestrutura que todo domínio usa, mas que não pertence a nenhum domínio específico. É onde vive a “tomada de água e luz” do prédio: a camada que conversa com o backend, o tratamento global de erros, o monitoramento. Nenhuma regra de negócio mora aqui.
- checkout — o domínio de verdade. Carrinho, endereço, entrega, pagamento, clube de descontos. Toda a lógica que é específica de finalizar uma compra.
E o pulo do gato: a raiz “compõe” essas layers com uma única configuração que lista quais andares existem e em que ordem.
// configuração da raiz (resumida)
extends: [
'layer-corporativa', // auth e design system, vem de fora
'./layers/shared', // infraestrutura transversal
'./layers/checkout', // o domínio de checkout
]
Lembra do carrinho que vinha chegando? Aqui está o motivo dessa decisão valer ouro. No mundo antigo, encaixar o carrinho significaria abrir o código existente e ir costurando as coisas no meio do que já estava lá — alto risco de quebrar o checkout que já funcionava. No mundo de layers, adicionar o carrinho vira criar uma pasta nova e somar uma linha nessa lista. O checkout não precisa nem saber que ganhou um vizinho.
Nada disso é de graça, e seria desonesto fingir que é. Layer tem custo: existe uma curva de aprendizado pra saber em qual andar cada coisa vai, e a configuração fica espalhada (cada layer tem a sua). Mas é um custo que você paga uma vez, no começo, em troca de não pagar o custo de refatoração toda vez que o produto cresce. Pra um sistema que a gente sabia que ia crescer, a conta fechava fácil.
Segunda decisão: dar um cérebro, um corpo e uma memória pra cada tela
Resolver a separação entre domínios foi metade do caminho. A outra metade era arrumar a bagunça dentro de cada domínio — aquele arquivo de 200 linhas fazendo tudo.
Pra isso a gente adotou um padrão que internamente a gente chama de CAL (Composable Abstraction Layer). É uma velha conhecida de quem já trabalhou com interface: separar a coisa em três papéis claros. Se você quer entender mais fundo o pattern em si, escrevi sobre ele com detalhes em Composable Abstraction Layer: o pattern que faltava entre Pinia e seus componentes Vue.
Eu gosto de explicar assim, pensando numa pessoa:
- A View é o rosto. São as páginas e os componentes — o que aparece na tela. O rosto não toma decisões. Ele mostra o que mandam mostrar e avisa quando alguém clicou em algo. Só isso.
- O Composable é o cérebro. É onde mora o raciocínio: buscar os dados do passo, decidir o que fazer quando o usuário envia o formulário, escolher pra qual tela ir, disparar os eventos de analytics. Toda decisão acontece aqui.
- A Store é a memória. Ela só guarda o estado. Não busca nada, não decide nada, não navega. Pergunte a ela “o que está na tela agora?” e ela responde. Mais que isso, ela não faz.
A regra que amarra tudo é absoluta e fácil de lembrar: o rosto só conversa com o cérebro. Uma página nunca fala direto com a memória nem vai buscar dados sozinha. Ela sempre passa pelo composable.
Na prática, a diferença é gritante. Antes, a página era um caldeirão:
// ANTES — a página faz tudo
const orderStore = useOrderStore() // fala direto com a memória
const data = await useQuery(deliveryQuery) // busca dados sozinha
if (data.step === 'complete') {
navigateTo('/sucesso') // decide navegação
}
sendAnalytics('delivery_viewed') // dispara analytics
// ...mais 180 linhas
Depois, a página virou um roteiro de uma linha só — ela pede tudo pronto ao cérebro e só cuida de desenhar a tela:
// DEPOIS — a página (o rosto) só consome o cérebro
const {
loading,
deliveryStep,
plannedSteps,
handleUpdateDelivery,
} = await useDeliveryStepPage()
Toda a inteligência — a busca, o if da navegação, o analytics — foi pra dentro do useDeliveryStepPage. A página não sabe mais como as coisas acontecem. Ela só sabe o que mostrar. E a store voltou a ser só memória: tiramos dela a função que chamava a API e a função que navegava, e mandamos cada uma pro cérebro a que pertenciam.
O ganho mais palpável disso apareceu nos testes. Pra testar o cérebro, você não precisa mais montar a tela inteira. Você troca a camada de dados por uma versão de mentira, chama a função diretamente e verifica se ela decidiu certo. Aquele teste que antes “dava trabalho demais” virou três linhas. E teste que é barato de escrever é teste que de fato existe.
A jogada que faz a diferença: parar de pedir, começar a impedir
Aqui chega a parte de que eu mais gosto de contar, porque é a que mais gente esquece.
A gente podia ter parado no parágrafo anterior. Layers organizadas, código separado em rosto, cérebro e memória, tudo bonito. Mas você lembra qual era a dor que de verdade tirava o sono? Não era a bagunça em si. Era o fato de que a bagunça se reproduzia sozinha. Documentação não tinha segurado antes. Por que seguraria agora?
A resposta foi tirar a regra do campo da boa vontade e colocá-la no campo da automação. A gente ensinou o linter — a ferramenta que confere o código automaticamente a cada mudança — a barrar as violações na hora. Se alguém escrever uma página que fala direto com a memória, ou que busca dados por conta própria, o robô do build reprova. Não passa. Não tem revisor distraído que deixe escapar, não tem “depois eu arrumo”.
// o linter passou a reprovar isto dentro de páginas e componentes:
// - chamar uma store diretamente
// - buscar dados diretamente (useQuery / useMutation)
// - fazer requisição HTTP direta ($fetch)
A frase que eu carrego dessa fase é mais ou menos essa: convenção que depende de disciplina humana é convenção que vai morrer numa sexta-feira às 18h. Toda equipe é disciplinada quando ninguém está com pressa. Mas quando estão, ninguém precisa policiar ninguém. O robô faz o trabalho chato, e os humanos discutem o que importa de verdade.
A fuga das dependências instáveis
Tem uma decisão mais técnica que vale registrar porque ela revela uma filosofia.
Pra conversar com o backend, a gente tinha duas escolhas. A primeira era usar um módulo pronto, muito popular, que faz essa ponte com pouco esforço — mas que vinha nos dando dor de cabeça: versões em beta, atualizações que quebravam, tratamento de erro frágil. A segunda era usar uma biblioteca mais simples e estável pra fazer a chamada, e construir nós mesmos a fininha camada de cima.
A gente escolheu a segunda. À primeira vista parece contraintuitivo — por que escrever mais código quando existe algo pronto? A razão é uma só: não queríamos amarrar a saúde do nosso checkout à saúde de uma dependência que a gente não controla. O módulo pronto era confortável até o dia em que ele decidisse não funcionar mais com a próxima versão do framework. Aí o problema viraria nosso, na pior hora possível.
Junto disso veio uma segunda regra: o cliente — o código que roda no navegador da pessoa — nunca chama o backend diretamente. Toda chamada passa por uma camada intermediária que roda no nosso próprio servidor, um padrão conhecido como BFF (Backend for Frontend). Pense nela como um recepcionista: o navegador faz o pedido pro recepcionista, e é o recepcionista quem fala com os serviços lá dentro, colocando as credenciais certas e cuidando dos detalhes chatos. O navegador nunca precisa conhecer os corredores internos do prédio.
As duas decisões vêm do mesmo princípio: dependa de coisas que você controla, e ponha fronteiras claras entre o seu código e o mundo lá fora. Conforto de curto prazo raramente vale o risco de longo prazo num sistema que precisa ficar de pé.
O que tudo isso custou
Eu desconfio de qualquer artigo de arquitetura que só conta a parte boa. Toda decisão tem um preço, e esconder o preço é fazer marketing, não engenharia. Então aqui vão os nossos.
Mais arquivos por tela. No padrão novo, cada passo do checkout virou três arquivos: a página, o cérebro e o teste do cérebro. Pra quem está acostumado com um arquivo só, parece burocracia. Na prática, é o oposto: tudo que é de uma tela está junto, num diretório só. Apagar a tela é apagar a pasta. Mas é um custo real de “mais coisas pra abrir”.
Camadas que parecem redundantes. Algumas pontes entre o cérebro e a memória são finíssimas — quase parecem código a troco de nada. Elas existem de propósito: são o ponto onde a gente garante que o rosto nunca dependa dos detalhes internos da memória. É uma indireção intencional. Custa um pouco de paciência pra entender por que ela está ali.
A migração foi longa e desconfortável. A gente não parou tudo pra reescrever — isso seria suicídio num sistema em produção. A virada foi feita em fases, e durante um bom tempo o código novo conviveu com o código velho lado a lado. Foi um período em que o projeto teve dois sotaques ao mesmo tempo, e isso confunde. Mas era o preço de não parar de vender.
Pra amaciar esse custo, a gente automatizou a criação dos arquivos repetitivos. Em vez de o dev montar o esqueleto de uma tela nova na mão — e errar o padrão —, uma ferramenta gera tudo já no formato certo. O custo do boilerplate vira o custo de rodar um comando.
Os números, pra não ficar no “achismo”
Toda essa conversa seria papo de boteco se não desse pra medir. Deu. Quando a gente compara o antes e o depois da reorganização, os números contam a mesma história que a sensação no dia a dia:
| Dimensão | Antes | Depois |
|---|---|---|
| Linhas de código por página (média) | ~200 | ~80 |
| Arquivos de teste no projeto | ~54 | 109 |
| Quantidade total de testes | ~636 | 791 |
| Stores com efeito colateral | 2 | 0 |
| Barreiras automáticas no build | 0 | 3 |
A página média encolheu pra menos da metade — não porque a gente jogou código fora, mas porque tirou da tela o que nunca foi dela. O número de testes subiu de uns 636 pra quase 800, e isso não foi força de vontade: foi consequência direta de o código ter ficado testável. E as duas stores que faziam coisa que não deviam viraram zero. A bagunça que antes se reproduzia sozinha agora bate numa parede automática toda vez que tenta voltar.
O teste de verdade: o carrinho chegando
Toda arquitetura parece ótima no dia em que você termina de escrevê-la. O teste real é o que acontece quando o mundo muda e você precisa mexer nela de novo.
Pro Atlas, esse teste é o carrinho. Aquele domínio que a gente sabia que vinha chegando desde o começo da história.
No Atlas de 200 linhas por arquivo, com tudo grudado na raiz e regra de negócio escondida dentro das stores, encaixar o carrinho seria um projeto de risco: abrir o checkout que já funciona, costurar o código novo no meio, torcer pra não derrubar nenhuma venda. O tipo de tarefa que tira o sono de qualquer tech lead.
No Atlas de hoje, o carrinho é uma layer nova. Uma pasta, uma linha na lista de andares do prédio, e ele nasce isolado — com suas próprias páginas, seu próprio estado, seus próprios testes — sem encostar numa única linha do checkout. As barreiras automáticas garantem que ele já nasça seguindo o padrão certo, sem ninguém precisar lembrar de nada. O recepcionista que conversa com o backend já está lá, pronto pra ser reusado.
A gente não arrumou a casa só pra ela ficar bonita. A gente transformou a casa num prédio que aceita andares novos. E é por isso que, quando o carrinho chegar, ele vai entrar pela porta da frente — e não arrombando a parede do checkout.
O que eu levaria pra qualquer time
Se você ficou só com quatro ideias dessa história, que sejam estas:
Arquitetura boa é a que torna a próxima mudança barata. Não meça pela elegância do diagrama. Meça por quanto dói adicionar a próxima coisa. Se doer pouco, você acertou.
Convenção sem ferramental é só um pedido. Disciplina humana falha exatamente na hora da pressa. Se uma regra importa de verdade, faça o build reprovar quando ela for quebrada. Pare de pedir, comece a impedir.
Separe quem mostra de quem decide de quem lembra. Rosto, cérebro e memória. É um padrão velho porque funciona. Tela burra, lógica concentrada num lugar, estado que só guarda estado — e tudo de repente fica testável.
Conte o preço, não só o prêmio. Toda decisão de arquitetura tem trade-off. O time que conhece o custo da própria escolha é o time que sabe quando voltar atrás. Esconder o custo não faz ele sumir; só faz a surpresa chegar mais tarde.
A gente trocou o motor do avião em pleno voo. Ninguém em terra percebeu — as compras continuaram acontecendo o tempo todo. E quando o próximo motor precisar entrar, dessa vez vai ter um lugar certo pra ele encaixar.
Isso, pra mim, é o que arquitetura deveria ser: não o castelo perfeito que você admira de longe, mas a casa que continua boa de morar mesmo quando a família cresce.