Dev Life
Um mergulho profundo na reconstrução do aplicativo Mailgun: Parte 1 – Ideias excelentes
Saudações, colegas desenvolvedores. Estamos empolgados em anunciar que, após três longos anos de planejamento, design e construção, nossa equipe lançou nossa fantástica atualização de aplicativo. Claro, temos trabalhado incansavelmente para atualizar a aparência de nossa interface de usuário, mas fizemos uma reformulação ainda mais ousada em nossa pilha de tecnologia fundamental.
O que isso significa para você? Melhor desempenho, melhor interface e escalabilidade para crescimento futuro. Estamos viajando de volta no tempo para trazer a você o relatório completo sobre como fizemos isso acontecer. Junte-se a nós nesta jornada.
Por que embarcamos nesta jornada
Mas espere! Antes de voltarmos no tempo, é importante apresentar nosso aplicativo melhorado. “Melhorado” é um eufemismo. Pode surpreendê-lo que esta renovação de interface e desempenho não foi um impulsionador de receita. Então, por que fizemos isso?
Dois motivos:
- A experiência do usuário, como existia, inibia o desempenho para os clientes que enviavam grandes volumes de e-mails e tornava desafiador navegar entre os produtos de nosso crescente portfólio. Se não for fácil de usar, então você não usará.
- Precisávamos reavaliar nosso fluxo de dados central da API para ser mais orientado ao desempenho e escalável.
O que criamos é o portal, uma interface épica para conectar você melhor aos seus produtos e ferramentas de entregabilidade agora, e à medida que crescemos. O portal permite que você alterne entre nosso Mailgun e Mailgun Optimize nossos produtos com facilidade e oferece uma experiência de alto desempenho para nossos usuários avançados que enviam dezenas de milhares de e-mails por dia.
Então, fique conosco enquanto mergulhamos nos detalhes técnicos, nos aprofundamos e viajamos pelos últimos três anos de nosso processo de desenvolvimento.

O que queríamos construir
Começamos criando uma longa lista de todas as coisas diferentes que queríamos – desde a atualização da estrutura do nosso repositório até a implementação de um novo sistema de implantação, e sabíamos que teríamos que criá-lo do zero enquanto nossa equipe de SRE migrava da AWS para o Google Cloud. Também queríamos abandonar nosso design monolítico fortemente acoplado por um design mais distribuído. Isso facilitaria tudo para a equipe daqui para frente: desde o desenvolvimento de testes abrangentes até o direcionamento de reconstruções sem a necessidade de vasculhar ou alterar todo o código.
Em qualquer reformulação, sempre queremos considerar o usuário, mas grande parte da motivação por trás da atualização do nosso aplicativo também foi melhorar a experiência do desenvolvedor. Nosso objetivo era aprimorar a experiência do desenvolvedor reduzindo significativamente a complexidade de obter dados da API para a página renderizada, e melhorar a capacidade de manutenção diminuindo o número total de padrões em nosso conjunto de ferramentas.
História: Nossa pilha original de front-end
As pilhas de tecnologia podem ficar desatualizadas, e nossa pilha de front-end original foi construída há quase uma década – o que equivale a 350 em anos de internet. Ela incluía um servidor web Python Flask que provavelmente lidava com mais tarefas do que deveria e tinha um estado fortemente acoplado compartilhado com o cliente. Ela usava várias camadas de abstração, originalmente destinadas a facilitar a obtenção de dados de nossas APIs.
À medida que essa superfície crescia cada vez mais, os padrões forneciam mais sobrecarga do que conveniência. Tornou-se muito difícil fazer mudanças fundamentais porque havia muitos lugares que poderiam ser impactados. Neste momento, nosso cliente front-end havia se tornado um museu de padrões obsoletos do React.
Todas as nossas interações com nossas próprias APIs públicas e privadas foram ofuscadas por meio de múltiplas camadas de complexidades excessivas de código. Isso criou um abismo gigantesco entre os desenvolvedores de front-end do Mailgun e a compreensão deles sobre nossas APIs e as experiências do cliente.
Como nossa estrutura legada prejudicou nossa escalabilidade
A gambiarra de abstrações de API não apenas resultou em complexidade excessiva, mas também inibiu significativamente nossa capacidade de dimensionar nosso aplicativo. Sabíamos que, eventualmente, queríamos trazer cada vez mais produtos para a base de código. Precisaríamos repensar totalmente a forma como consumíamos nossos dados.
Ao fazer nosso plano para a nova estrutura, sabíamos que queríamos ser capazes de fazer solicitações de API diretamente do cliente para as APIs públicas com o mínimo de middleware possível. Por quê? Desta forma, seríamos capazes de entender e consumir nossas APIs da mesma forma que nossos clientes fazem, “comendo nossa própria ração” (eating our own dog food), como dizem. Eu não sei quanto a você, mas prefiro comer bife a comida de cachorro.
O problema em gerenciar estados em todo o aplicativo
Outra mudança significativa que queríamos fazer era substituir o Redux, que gerenciava o estado de todo o nosso aplicativo (cerca de 150 estados). Cada estado era mapeado para uma chamada de rede, ou rede de transformações de dados, e nossa estrutura legada não estabelecia diretrizes reais sobre como usá-lo. O resultado? Muitas redundâncias.
Quando um desenvolvedor introduzia uma chamada para manipular dados, ele a integrava através de três níveis de abstração. Além disso, tínhamos nossos próprios clientes personalizados para nosso navegador e nosso servidor Flask (que interagia com todas as nossas APIs). Portanto, para um desenvolvedor adicionar um novo recurso, ele teria que passar por cerca de sete níveis totais de abstração, e os bugs são possíveis em qualquer nível.
O que acabamos descobrindo foi que 95% do estado de todo o aplicativo era apenas um cache de rede glorificado. Só que não estávamos nem colhendo os benefícios dos dados em cache, muitas vezes fazendo chamadas redundantes para solicitar dados que já tínhamos, causando camadas aninhadas de re-renderizações desnecessárias em nossos componentes de visualização. Isso que é uma jornada acidentada.
Este detalhamento do sistema explica as coisas com um pouco mais de clareza e, embora nunca falássemos mal de nossas origens… uma imagem jocosa vale mais que mil palavras.
Sistema atual
Nossa equipe é o tipo de equipe que evolui e itera, mas para essa reconstrução do aplicativo, tivemos que voltar atrás e refazer alguns componentes fundamentais de nossa infraestrutura – começando pelo controle de código-fonte.
Limitações do polyrepo
Um polyrepo é um repositório que contém vários projetos. Nós usamos um estilo de organização polyrepo, o que significa apenas que cada projeto de front-end tinha seu próprio repositório, embora muitos dos projetos repetissem as mesmas tarefas e reproduzissem os mesmos recursos. Como um hub central, os polyrepos podem se tornar um desafio de manter à medida que o número de projetos armazenados aumenta. Em muitos casos, tivemos que implantar cada polyrepo simultaneamente com as mesmas atualizações para garantir uma experiência consistente em nossos produtos.
Do ponto de vista da manutenção, isso dificultou que as equipes localizassem e trabalhassem com o código de que precisam. O gerenciamento de vários repositórios com recursos redundantes geralmente significa que eles respondem mais lentamente e consomem mais recursos, o que é um grande problema se você faz push e pull de código com frequência. Como os recursos podem ser distribuídos por várias bases de código, os polyrepos podem tornar a localização de bugs mais difícil do que encontrar a proverbial agulha no palheiro.
Com nossa estrutura polyrepo anterior à reconstrução, sabíamos que o futuro seria uma realidade de vasculhar repositórios com uma lanterna procurando pelo que precisávamos, e que seria desafiador colaborar, dimensionar e compartilhar o código à medida que crescíamos.
Estas eram nossas principais preocupações:
- Desenvolvimento conjunto desafiador
- Forte acoplamento
- Escalabilidade, disponibilidade e gerenciamento de desempenho
- Dificuldade de compartilhar código
- Duplicação
- Ferramentas inconsistentes
Quando se trata de gerenciamento de código-fonte, há muitas soluções, mas decidimos que uma estrutura modular de monorepo aliviaria a maioria de nossas dores de desenvolvimento no futuro.
Planos para um novo repositório brilhante: uma abordagem modular
Uma abordagem de monorepo envolve o armazenamento de todo o código do projeto em um único e grande repositório. Isso resolveria os problemas de compartilhamento e duplicação de código que tivemos com nossa organização polyrepo, mas os monorepos não são uma solução mágica. Se o código for fortemente acoplado, os monorepos também precisarão ser projetados com modularidade.
Uma abordagem modular envolve dividir um grande projeto em módulos menores e independentes que podem ser desenvolvidos, testados e mantidos separadamente. Isso permite maior flexibilidade e capacidade de reutilização, bem como a capacidade de atualizar facilmente módulos individuais sem afetar o resto do projeto.
Sabíamos que uma solução modular e em monorepo nos permitiria ser mais eficientes com:
- Geração de código
- Compartilhamento de código
- Execução e orquestração de tarefas distribuídas
- Armazenamento em cache
- TypeScript
Pisando em calos: a dança da implantação
Sem dúvida, o maior desafio de viver em um monorepo (monolítico ou não) é que, quanto mais pessoas contribuírem, maior será o potencial de pisarmos nos calos uns dos outros. De que adianta uma experiência de desenvolvimento reformulada se o seu código for substituído em uma mesclagem ruim? Se realmente estamos fazendo uma reformulação holística de nossa pilha, também devemos analisar como podemos melhorar nosso processo de implantação. Esses foram os desafios mais urgentes:
- Escalabilidade: Seria uma má ideia reconstruir TODO o código no monorepo para as menores alterações, digamos, corrigir um erro de digitação em algum texto. Era exatamente isso que estávamos fazendo em nosso sistema antigo. O novo processo aproveitaria os recursos integrados do NX para avaliar e reconstruir apenas os arquivos afetados pela alteração, no nível mais granular.
- Implantação: Mantivemos a maior parte da estrutura do nosso pipeline de implantação. Uma vez que a imagem é construída, ela vai para o GitHub Container Repository. Nossa equipe de SRE ajustou nosso sistema de implantação durante a migração da AWS para o GCP. Podemos implantar facilmente qualquer ramificação do nosso canal de implantação no Slack, com um relatório mais agradável graças a uma adição recente que obtivemos durante a migração.
- CI/CD: Nosso sistema antigo usava uma versão realmente antiga do Jenkins para executar testes e construir/distribuir nossas imagens de aplicativo por trás de uma VPN. Quando a compilação falhava, precisávamos de um desenvolvedor sênior de alto nível, familiarizado com as antigas formas, para consertá-la. Em nosso novo sistema, tínhamos o plano de migrar para o GitHub Actions, onde as configurações são acessíveis, declarativas e bem documentadas.
Desafios falsos e soluções excelentes
Projetos de qualquer tipo apresentam desafios, tanto os esperados quanto os surpreendentes. Sabíamos que precisávamos ter um plano para superar alguns problemas e que a nova versão do aplicativo precisava focar no desempenho sob vários ângulos.
| Problemas identificados | Soluções propostas |
|---|---|
| Convenções de código pouco claras As convenções de código referem-se ao conjunto de estilos de programação, soluções e padrões reutilizáveis dentro de uma base de código. Imagine uma equipe de carpinteiros trabalhando em um projeto. Se eles mantiverem 1.000 ferramentas especializadas no local de trabalho, é provável que um trabalhador escolha a ferramenta errada para o trabalho ou a utilice de maneira inadequada. E, ao contrário das ferramentas físicas, as dependências de software podem se tornar desatualizadas, inibindo futuras atualizações na base de código. | Guia de estilo de código Manter guias explícitos de estilo de programação e um conjunto mínimo de padrões simples e poderosos ajuda a reduzir a redundância, inconsistências e complexidade, e pode até resultar em desempenho melhorado. Reutilizar um pequeno conjunto de soluções e padrões generalizados torna o código mais fácil de entender e reduz a probabilidade de bugs difíceis de encontrar. |
| Reutilizar um pequeno conjunto de soluções e padrões generalizados torna o código mais fácil de entender e reduz a probabilidade de bugs difíceis de encontrar. No entanto, 90% do nosso armazenamento Redux era apenas um cache de rede glorificado. O Redux exige múltiplas camadas de sobrecarga para cada membro do estado. Isso resultou em muitas redundâncias e complexidade desnecessária. | Separar o cache da rede e o estado do aplicativo A separação dos recursos de cache de rede do estado do aplicativo nos permite manter um rastro de estado muito menor. O estado pode ser implementado mais localmente. A sobrecarga reduzida resulta em maior desempenho e dá aos desenvolvedores mais controle sobre como os dados são usados. Nós escolhemos o React Query como a interface para nossas APIs de back-end, permitindo gerenciarmos os dados de forma assíncrona com facilidade e reduzir a sobrecarga de rede. |
| Arquitetura de rede em camadas múltiplas Nosso aplicativo cresceu organicamente de um painel de controle básico centrado na API para o pacote multiproduto de ferramentas para clientes que temos hoje. Inicialmente, o cliente foi construído sobre ferramentas comuns de Python compartilhadas por muitos dos nossos back-ends de API. Isso leva a termos inúmeras camadas de middleware necessárias mesmo para a chamada de API mais simples. Isso não apenas dificultou o entendimento da origem de qualquer dado, como também tornou a identificação de bugs na pilha de rede muito demorada. | API pública primeiro Em vez de passar as solicitações por vários proxies, camadas de autenticação e interfaces em Python, decidimos logo no início que sempre que possível, coletaríamos os dados necessários diretamente do cliente usando as APIs disponíveis publicamente. Isso não apenas reduziu significativamente nossa sobrecarga e complexidade, como nos ajudou a perceber melhor nossos serviços de API a partir das perspectivas de nossos clientes. Nós pretendemos levar essa iniciativa adiante no futuro próximo, tornando mais das nossas APIs públicas. |
Não jogue o bebê fora junto com a água do banho
Escrevemos centenas de milhares de linhas de código na última década. Escrevemos centenas de milhares de linhas de código na última década. Isso seria inviável. Precisávamos encontrar uma maneira de continuar incorporando esses recursos existentes sem comprometer nossos objetivos para a nova pilha. Encontramos uma solução em uma tecnologia chamada module federation.
Com o plugin de module federation do Webpack, pudemos criar uma nova compilação do nosso aplicativo antigo com mudanças mínimas, o que seria capaz de integrar os recursos existentes na nova pilha do portal. Ainda teríamos um objetivo de longo prazo de portar o código agora legado para padrões mais recentes, mas isso nos permitiria começar a escrever novos recursos com os novos benefícios imediatamente.
Ainda teríamos um objetivo de longo prazo de portar o código legado para padrões mais recentes, mas isso nos permitiria começar a escrever novos recursos com os novos benefícios imediatamente.
Sabíamos que consolidar nossas plataformas enquanto ampliávamos continuamente os recursos e conjuntos de produtos exigiria um esforço maciço, e nosso planejamento influenciou o que queríamos para nossa nova pilha de front-end, e como gerenciaríamos o legado imortal de sistemas de estilo CSS antiquados, padrões de componentes de classe descontinuados e gerenciamento desajeitado do estado do aplicativo.
Como estávamos construindo do zero – especialmente com a migração da AWS para o Google Cloud – a melhor solução foi uma ruptura total. Um novo repositório com padrões melhores e mais elevados, module federation, bibliotecas modulares e compartilhadas, e uma documentação interna e voltada para o usuário mais abrangente.

Foi uma etapa intensa de planejamento e preparação. Foi uma etapa intensa de planejamento e preparação.
Fase 2? Transformar a ideia em realidade – e isso envolveu muito mais equipes. Desenvolvedores, acredito que nossa aventura está prestes a tomar um rumo muito interessante.