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 […]
Image for 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:

  1. 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á.
  2. 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.

bill-teds-excellent-adventure-wallpaper-12__1_

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.

“O que queremos construir no final das contas?” Esta foi a primeira pergunta que tivemos que responder. A próxima pergunta foi: “Onde estamos deixando a desejar?”

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.

Uma nota do futuro: Fazer solicitações de API diretamente do cliente para as APIs públicas é um objetivo a longo prazo, já que muitas das APIs com as quais nosso aplicativo web se comunica ainda são privadas. Temos o plano de mover gradualmente a maioria desses serviços para o espaço público, a fim de ter uma interface de API uniforme para o portal.

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
O TypeScript é um superconjunto de JavaScript tipado que facilita aos desenvolvedores a construção de projetos em escala. Ele melhora a experiência do desenvolvedor ao fornecer interfaces, aliases de tipo, análise estática de código em tempo de desenvolvimento e outras ferramentas, permitindo que os desenvolvedores adicionem tipos aos seus projetos. Para nossa equipe, o TypeScript é um conjunto de habilidades que sabíamos que queríamos começar a buscar quando se tratava de adicionar membros à nossa equipe.

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.

Pro­ble­mas iden­ti­fi­ca­dosSo­lu­ções pro­pos­tas
Con­ven­ções de có­di­go pou­co cla­ras
As con­ven­ções de có­di­go re­fe­rem-se ao con­jun­to de es­ti­los de pro­gra­ma­ção, so­lu­ções e pa­drões reu­ti­li­zá­veis den­tro de uma ba­se de có­di­go. Ima­gi­ne uma equi­pe de car­pin­tei­ros tra­ba­lhan­do em um pro­je­to. Se el­es man­ti­ve­rem 1.00­0 fer­ra­men­tas es­pe­ci­a­li­za­das no lo­cal de tra­ba­lho, é pro­vá­vel que um tra­ba­lha­dor es­co­lha a fer­ra­men­ta er­ra­da pa­ra o tra­ba­lho ou a uti­li­ce de ma­nei­ra in­a­de­qua­da. E, ao con­trá­rio das fer­ra­men­tas fí­si­cas, as de­pen­dên­cias de soft­wa­re po­dem se tor­nar de­sa­tu­a­li­za­das, ini­bin­do fu­tu­ras atu­a­li­za­ções na ba­se de có­di­go.
Gu­ia de es­ti­lo de có­di­go
Man­ter gu­ias ex­plí­ci­tos de es­ti­lo de pro­gra­ma­ção e um con­jun­to mí­ni­mo de pa­drões sim­ples e po­de­ro­sos aju­da a re­du­zir a re­dun­dân­cia, in­con­sis­tên­cias e com­ple­xi­da­de, e po­de a­té re­sul­tar em de­sem­pe­nho me­lho­ra­do. 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.
Reu­ti­li­zar um pe­que­no con­jun­to de so­lu­ções e pa­drões ge­ne­ra­li­za­dos tor­na o có­di­go mais fá­cil de en­ten­der e re­du­z a pro­ba­bi­li­da­de de bugs­ di­fí­ceis de en­con­trar. No en­tan­to, 90% do nos­so ar­ma­ze­na­men­to Re­dux­ era a­pe­nas um ca­che de re­de glo­ri­fi­ca­do. O Re­dux­ e­xi­ge múl­ti­plas ca­ma­das de so­bre­car­ga pa­ra ca­da mem­bro do es­ta­do. Is­so re­sul­tou em mui­tas re­dun­dân­cias e com­ple­xi­da­de des­ne­ces­sá­ria.Se­pa­rar o ca­che da re­de e o es­ta­do do a­pli­ca­ti­vo
A se­pa­ra­ção dos re­cur­sos de ca­che de re­de do es­ta­do do a­pli­ca­ti­vo nos per­mi­te man­ter um ras­tro de es­ta­do mui­to me­nor. O es­ta­do po­de ser im­ple­men­ta­do mais­ lo­cal­men­te. A so­bre­car­ga re­du­zi­da re­sul­ta em mai­or de­sem­pe­nho e d­á aos de­sen­vol­ve­do­res mai­s con­tro­le so­bre co­mo os da­dos são usa­dos.
Nós es­co­lhe­mos o Re­act­ Que­ry co­mo a in­ter­fa­ce pa­ra nos­sas APIs de back-end­, per­mi­tin­do ge­ren­ci­ar­mos os da­dos de for­ma as­sín­cro­na com fa­ci­li­da­de e re­du­zir a so­bre­car­ga de re­de.
Ar­qui­te­tu­ra de re­de em ca­ma­das múl­ti­plas
Nos­so a­pli­ca­ti­vo cres­ceu or­ga­ni­ca­men­te de um pai­nel de con­tro­le bá­si­co cen­tra­do na API­ pa­ra o pa­co­te mul­ti­pro­du­to de fer­ra­men­tas pa­ra clien­tes que te­mos ho­je. In­i­ci­al­men­te, o clien­te foi cons­tru­í­do so­bre fer­ra­men­tas co­muns de Py­thon com­par­ti­lha­das por mui­tos dos nos­sos back-ends de API. Isso leva a termos inúmeras camadas de middleware necessárias mesmo para a chamada de API mais simples. Is­so não a­pe­nas di­fi­cul­tou o en­ten­di­men­to da o­ri­gem de qual­quer da­do, co­mo tam­bém tor­nou a i­den­ti­fi­ca­ção de bugs­ na pi­lha de re­de mui­to de­mo­ra­da.
API pú­bli­ca pri­mei­ro
Em ve­z de pas­sar as so­li­ci­ta­ções por vá­rios pro­xies, ca­ma­das de au­ten­ti­ca­ção e in­ter­fa­ces em Py­thon, de­ci­di­mos lo­go no in­í­cio que­ sem­pre que pos­sí­vel, co­le­ta­rí­a­mos os da­dos ne­ces­sá­rios di­re­ta­men­te do clien­te u­san­do as APIs­ dis­po­ní­veis pu­bli­ca­men­te. Is­so não a­pe­nas re­du­ziu sig­ni­fi­ca­ti­va­men­te nos­sa so­bre­car­ga e com­ple­xi­da­de, co­mo­ nos a­ju­dou a per­ce­ber me­lhor nos­sos ser­vi­ços de API­ a par­tir das pers­pec­ti­vas de nos­sos clien­tes. Nós pre­ten­de­mos le­var es­sa i­ni­ci­a­ti­va a­di­an­te no fu­tu­ro pró­xi­mo, tor­nan­do mai­s das nos­sas APIs pú­bli­cas.

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.

New front-end stack; Nx, React, Webpack, TypeScript, Cypress, Jest, ES Links, Prettier, and Mock Service Worker.

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.