- A adoção de um monorepo traz vantagens como consistência organizacional, compartilhamento de código e fortalecimento de um ambiente de ferramentas em comum, mas copiar diretamente os casos das big techs acaba levando a novos problemas e desafios
- Para um monorepo bem-sucedido, é preciso seguir o princípio de fazer com que todas as tarefas principais sejam O(change), e não O(repo), exigindo ferramentas e estratégias adequadas em cada etapa de build, testes e CI/CD
- Em controle de código-fonte, o ponto de partida é o Git, mas conforme a escala cresce é preciso considerar expansões graduais como sparse checkout e sistemas de arquivos virtuais
- O sistema de build deve permanecer, sempre que possível, em uma única linguagem, sustentando-se o máximo possível nas ferramentas nativas de build de cada linguagem e migrando gradualmente para Bazel/Buck2 apenas quando realmente necessário
- Em testes e CI/CD, é necessário detectar rapidamente apenas o escopo impactado pelas mudanças para buildar, testar e implantar, e em monorepos de grande porte também são indispensáveis estratégias de confiabilidade como retry automático de testes e isolamento de flaky tests
Introdução: o início da jornada rumo ao monorepo
- Para engenheiros em uma nova equipe de Developer Productivity, a preocupação cresce em torno de quais preparações e esforços são necessários depois da decisão de adotar um monorepo
- As boas práticas de grandes empresas como Google, Meta e Uber parecem impressionantes, mas na prática é impossível obter o mesmo nível de resultado que elas
- Cada organização deve decidir pela adoção de um monorepo com base em suas próprias razões e necessidades, e nesse processo pode buscar vantagens em consistência, integração organizacional e ferramentas compartilhadas
Tornando clara a necessidade de um monorepo
- Os casos de grandes empresas são apenas o estado a que elas chegaram no fim, e não servem bem como referência para o começo
- Na prática, surgem novos problemas, e aparecem tipos de questões diferentes das de um sistema tradicional de vários repositórios
- O objetivo de adotar um monorepo está em manter consistência, integrar ferramentas em toda a organização e aplicar padrões e convenções de engenharia
- Cada equipe precisa deixar claros os objetivos alinhados à sua própria cultura e direção para obter resultados eficazes
Regra de ouro: o princípio de O(change)
- Todas as ferramentas relacionadas ao repositório devem ter complexidade O(change), e não O(repo), para funcionarem rapidamente
- Na prática, quanto maior o monorepo em larga escala, mais evidentes ficam as ineficiências das ferramentas existentes, tornando essencial um desenho estrutural para superar os problemas de desempenho
- As inovações mencionadas nos blogs técnicos das grandes empresas também se concentram, em sua maioria, em superar as ineficiências causadas por O(repo)
Controle de código-fonte
- A maioria das organizações de software usa Git como base, mas o Git tem limites de desempenho quando precisa escalar em um ambiente de monorepo centralizado
- Na prática, a maioria das organizações consegue ir bastante longe usando git+GitHub
- À medida que o crescimento acelera, passam a ser necessárias estruturas como sparse checkout (clone parcial) e sistemas de arquivos virtuais (download dinâmico de arquivos do servidor quando necessário)
- Grandes empresas fazem fork do Git ou desenvolvem sistemas separados para isso (Microsoft: fork próprio do Git, Meta: fork do Mercurial, Google: Piper etc.)
- Também vale considerar controles de código-fonte de nova geração, como Jujutsu
- Em escala pequena, é possível usar Git sem grandes dificuldades, mas é importante manter uma estratégia de expansão em mente ao longo do crescimento
- Há também o problema prático de que, quando o código-fonte inclui código gerado por IDL (Interface Definition Language), o tamanho do repositório pode crescer exponencialmente
Sistema de build
- Bazel e Buck2 são ferramentas representativas de build para monorepo, com suporte a várias linguagens e grafos de build complexos
- São poderosas, mas trazem grande complexidade e custo operacional
- Manter o build em uma única linguagem torna tudo muito mais simples, e os sistemas de build de cada linguagem (por exemplo: Maven, Gradle, Cargo, Go) também têm alta escalabilidade
- O papel central do sistema de build é “buildar de forma eficiente o target especificado (geração eficiente de artefatos)” e “determinar rapidamente os targets afetados por arquivos alterados”
- Para isso, é necessário o conceito de target determinator (ferramenta de determinação de targets), e já existem várias soluções nos ecossistemas de Rust, Go, Bazel etc.
- Execução remota e cache só se tornam realmente necessários em escala extremamente grande; para empresas comuns, a determinação de targets é mais prática
Testes
- Como executar todos os testes a cada vez é ineficiente, é necessário um sistema que teste apenas o escopo impactado pelas mudanças
- Flaky tests podem se tornar um problema ainda mais sério em sistemas de teste de grande escala
- O sistema de testes precisa de retry automático, avaliação automática do escopo impactado e isolamento de flaky tests
- Algumas linguagens (por exemplo, Rust com nextest, Java com JUnit etc.) oferecem esses recursos avançados por padrão ou por extensão
- A estrutura de testes de um monorepo precisa estar fortemente integrada ao sistema de build para ser eficaz
Integração contínua (CI)
- O sistema de CI deve executar automaticamente os artefatos de build e as validações necessárias de acordo com as mudanças
- O desempenho e a eficiência do target determinator funcionam como elementos centrais do pipeline de CI
- O CI moderno usa várias estratégias, como Merge Queue, para equilibrar manutenção da qualidade do código e otimização da velocidade de merge
- Por exemplo, decidir se toda validação será executada a cada commit/PR individual, se apenas parte dela será selecionada, ou se vários PRs serão processados em lote
- É preciso definir e projetar internamente os trade-offs entre throughput, correctness e tail latency
- O gerenciamento de merges e o aumento de eficiência de CI em monorepos muito grandes ainda são desafios sem solução perfeita
- Rust (bors), Chromium e Uber adotam, cada um, estratégias diferentes de merge/validação
Entrega contínua (CD)
- A ilusão de que toda mudança dentro do monorepo será implantada atomicamente não corresponde à realidade
- Em um único PR, é possível alterar de uma vez interfaces, implementações e até clientes de vários serviços, mas no fim a implantação real ocorre de forma assíncrona, o que pode gerar problemas no momento do deploy
- Mudanças que quebram contratos entre serviços podem causar falhas graves no momento da implantação
- Uma estratégia eficaz de CD para monorepo exige o ciclo do sistema de deploy, validação de contratos entre serviços e capacidade de detectar e responder rapidamente a problemas
Conclusão
- O monorepo é uma ferramenta poderosa para fortalecer a consistência organizacional e a cultura de engenharia, mas exige investimento contínuo em engenharia e ferramentas
- Em cada etapa, o ponto central é construir automação, ferramentas e cultura alinhadas ao princípio de O(change)
- À medida que a organização cresce, as ferramentas também devem evoluir continuamente, e é importante um esforço sistemático de gestão que reflita os objetivos e a cultura da organização
- Com preparo, comprometimento e investimento contínuo suficientes, o monorepo acaba entregando um valor compatível com esse esforço
4 comentários
É um texto realmente substancioso. Não basta ter ferramentas poderosas; é preciso estar preparado para criar até mesmo as ferramentas necessárias quando for preciso. Por isso, se tudo funcionar bem, os benefícios que se obtêm também são muitos.
Na época do mestrado, meu orientador foi almoçar com um engenheiro que tinha vindo do Google, ouviu falar sobre monorepo e voltou propondo que nós também passássemos a gerenciar tudo assim no futuro; foi um sufoco tentar dissuadi-lo...
Monorepo realmente tem muitas vantagens, mas no nosso laboratório, pela própria natureza do trabalho, era frequente precisarmos compartilhar os resultados com pessoas de fora, e acho que teríamos sofrido bastante justamente nessa parte se tivéssemos gerenciado esses resultados em um monorepo. Com multirepo, basta ajustar o nível de visibilidade de cada entrega separadamente.
A maioria dos casos em que se sofre com monorepo parece acontecer quando o projeto já foi fragmentado demais. Pegam um projeto que originalmente poderia ser um ou dois e dividem em uns 10; depois, ao tentar unificar e gerenciar tudo isso em um monorepo, acabam precisando usar também ferramentas de gestão de monorepo, e a complexidade aumenta. O melhor é integrar o próprio projeto em um ou dois, e mesmo que sejam mais de dois, em vez de usar uma ferramenta de gerenciamento separada, dá para pensar de forma mais simples: apenas dividir por diretórios e colocar tudo em um único repositório, o que torna a manutenção bem mais tranquila.
Comentários do Hacker News
Pedido de compartilhamento de experiências: esta thread fez lembrar as antigas conversas sobre os complexity merchants. Não concordo nem um pouco com a opinião de que migrar para um monorepo exige sacrifícios técnicos. Se você entende o poder de um sistema de arquivos hierárquico, entende o valor de um monorepo. CI/CD também fica muito mais claro quando organizado em um único monorepo, em vez de configurações espalhadas por todo lado. O ponto central do monorepo é que toda a organização pode fazer commits atômicos. Ao coordenar muitos desenvolvedores, a utilidade disso é esmagadora. Um único rebase e uma grande reunião bastam. Mesmo que os membros do time não se gostem nem colaborem entre si, do ponto de vista de gestão o monorepo funciona como uma grande ferramenta de RH.
Ultimamente os desenvolvedores têm uma tendência excessiva à separação, microservices, muitos repositórios pequenos e uma aversão extrema a monólitos. Isso aumenta a complexidade e transforma problemas de estrutura organizacional em problemas técnicos futuros. Também há uma incapacidade de reconhecer corretamente as dependências internas dos sistemas de software. No emprego anterior, o tempo desperdiçado para atualizar arquivos de esquema do Protocol Buffers era inacreditável. Felizmente, na empresa atual não é assim.
Rastrear commits em vários projetos é algo apenas desejável, e na prática não faz tanta diferença em termos de rastreamento de dependências ou disparo de testes downstream. Automação em multirepo também consegue resolver isso bem. O monorepo ajuda, mas não é completo e o custo é alto. Deploy e build não são processados atomicamente. Quando o monorepo cresce, é preciso sair do git e adotar novas ferramentas, e isso é um trabalho enorme. Não é algo sobre o qual se fala com facilidade sem experiência real.
As vantagens do monorepo certamente existem, mas o custo de gestão é mais alto do que no polyrepo. Monorepo não é automaticamente melhor em qualquer situação. Para uma explicação mais detalhada, veja este texto. A relação custo-benefício depende do contexto.
Uma regra prática útil no desenho de ambientes de programação é: quanto mais poder se dá ao time, mais problemas aparecem. Tecnicamente, commits atômicos não são um poder maior, mas sim menor; ainda assim, por possibilitarem trabalhar com interfaces ruins, acabam sendo um tipo de poder que gera problemas.
A crença de que, ao mudar para um monorepo, as mudanças se tornam mais atômicas é uma armadilha. [Citação do original: a maior ilusão do monorepo é achar que commits atômicos no codebase inteiro são possíveis. Na prática, existem vários artefatos de deploy; mesmo que serviços e clientes sejam alterados ao mesmo tempo, o deploy acontece de forma assíncrona. Em vários repositórios, é preciso trabalhar com vários PRs, então a percepção de risco já vem embutida. O CI do monorepo serve principalmente para validar contratos de serviço (jobs de CI) e, quando necessário, exige explicitar o motivo da mudança.]
Existem dois tipos de monorepo em big tech. O primeiro é o único monorepo corporativo, o "THE" monorepo citado no texto, que exige VCS/CI customizados e tem suporte de 200 engenheiros. Google, Meta e Uber seguem esse modelo. O sofrimento para chegar a esse nível vai muito além do imaginável, e normalmente começa com monorepos menores, por time, que vão se expandindo aos poucos. Cada stack/linguagem/time se gerencia com ferramentas como Bazel, Turborepo e Poetry, e com o tempo tudo acaba se juntando em um monorepo maior. Mas, em ambos os casos, tanto desenvolvedores quanto o negócio investem milhões de dólares e milhões de horas, e no fim tudo se sustenta com o apoio dos desenvolvedores que sobreviveram ao processo.
Quando trabalhei numa empresa com um monorepo grande, passei a preferir muito mais monorepo. Um monorepo único ajuda muito a enxergar com transparência o todo — o grafo de serviços, a estrutura de chamadas de código etc. No polyrepo, o conhecimento fica disperso por times, é difícil assumir código novo, e entender os arquivos de código parece explorar um labirinto. O polyrepo dá a sensação de mensagens antigas de Discord/Slack que acabam esquecidas. Se o monorepo é caro, o polyrepo também é, só que de outra forma. O monorepo é como um gigantesco herbívoro continental; o polyrepo é uma diversidade de espécies soterradas na escuridão.
Na empresa atual, o backend está dividido em cerca de 11 repositórios git, e para uma única funcionalidade são necessários 4 ou 5 merge requests, o que é muito incômodo. Estamos avaliando adotar monorepo para reunir vários projetos. Mas, se não for possível juntar os repositórios, qual seria a alternativa ao monorepo?
Ainda não existe um sistema de orquestração de monorepo fácil e poderoso, independente de linguagem. Bazel é complexo e difícil de aprender, embora a documentação tenha melhorado bastante ultimamente. Há outras opções como Buck, NX e Pants, mas cada uma tem suas características, e o suporte a web em especial é limitado. A maioria dos CIs não dá suporte adequado a essas ferramentas, então a configuração é trabalhosa. Como referência, o Rush da Microsoft oferece a melhor experiência; especialmente para monorepos de frontend/NodeJS, a recomendação é o site oficial do Rush.
Vale lembrar que a maioria dos monorepos não cresce até o porte de Google, Uber ou Meta. O número de serviços varia conforme a empresa e, mesmo que chegue a 100, isso ainda não gera problema de escala no VCS, e até tags de LSP rodam tranquilamente em um laptop. Mesmo rodar todos os testes indiscriminadamente no CI costuma ser aceitável. Conclusão: nem toda empresa precisa de escala de Google.
Na empresa atual, estamos montando monorepos por stack de linguagem. É um meio-termo bastante razoável.
Um ponto que quase nunca aparece na discussão monorepo vs multirepo é o surgimento da “lei de Conway reversa”. A estrutura dos repositórios influencia a estrutura organizacional e a forma de resolver problemas. Monorepo tende a gerar trabalho heroico para times de infraestrutura compartilhada; como há mais potencial de quebra ao mexer em áreas comuns, até desenvolver uma única funcionalidade fica mais difícil. No multirepo, são necessários vários PRs, coordenação entre times e política interna, mas uma variedade maior de desenvolvedores consegue distribuir esses papéis.
Mesmo em monorepo, se a mudança estiver profundamente conectada à parte central, ela pode ser aplicada em várias etapas. Nesse processo, também é preciso lidar com vários PRs, coordenação e questões políticas, mas justamente por ser monorepo fica mais claro visualizar a situação do rollout.
Em polyrepo, é muito mais comum que mudanças em áreas compartilhadas não sejam propagadas para repositórios downstream, de modo que cada repositório fica preso a uma versão diferente e passa anos sem atualização, causando sofrimento.
Fica a pergunta se faz sentido assumir que a organização escolhe primeiro a direção via estrutura de repositório e só depois as escolhas técnicas acompanham. Na prática, antes da estrutura concreta dos repositórios vem uma filosofia organizacional mais fundamental (fragmentação vs compartilhamento). Mesmo que a direção mude, a forma de gerenciar o código pode ser ajustada. Mesmo em multirepo, os engenheiros podem ter acesso a quase todo o código; e em monorepo também é possível aplicar isolamento forte, além de regras separadas de CI ou deploy.
Em monorepo, mudanças fáceis entre projetos acontecem com muito mais frequência; no polyrepo, muitas vezes isso é tão incômodo que nem se tenta.
Pela experiência em grandes empresas de tecnologia, a gestão do sistema de build exige um time dedicado. Monorepos grandes se baseiam em sistemas de arquivos virtuais que baixam os arquivos-fonte quando necessário. Um ponto não mencionado no artigo é que quase todo o desenvolvimento acontece em servidores de desenvolvimento no datacenter, usando ambientes com 50 a 100 cores ou containers sob demanda (atualizados continuamente para o commit mais recente). A IDE se integra ao dev server, e a preparação/configuração automática por linguagem ou serviço é automatizada com chef/ansible. É muito raro desenvolver diretamente um monorepo enorme no laptop (exceções: mobile, apps para Mac etc.).
Provavelmente alguém que trabalhou no mesmo time de build. Seja local ou remoto, em um ambiente de desenvolvimento para monorepo a reprodutibilidade é mais importante. Se o dev server remoto é baseado em imagens, fica ainda mais fácil e confiável.
Também há experiência usando ambiente de desenvolvimento em datacenter em times menores. Com os preços e a densidade do hardware hoje, faz muito mais sentido montar um rack próprio e rodar sob demanda ferramentas de dev/staging/test. Quando se compartilha um ambiente de desenvolvimento semelhante ao de produção, o modelo de monorepo parece bem diferente. Mas empresas pequenas e médias não têm fôlego para investir em sistema de build, e nesses casos o próprio problema de grandes sistemas de build nem chega a surgir (em equipes de no mínimo 10 a 20 pessoas, mesmo com um produto muito complexo, a manutenção pode continuar sendo só part-time).
Relato de uma equipe pequena no Molnett (serverless cloud), com 1,5 pessoa em tempo integral, que obteve enorme eficiência com um monorepo baseado em Bazel. Com Tilt+Bazel+Kind, conseguem subir no laptop a plataforma inteira e até o operador de Kubernetes, com suporte a Mac/Linux. Também conseguem validar localmente até o OS baseado em Bottlerocket e o Firecracker. Com a construção de uma camada de ferramentas, todos os desenvolvedores usam as mesmas versões de go/kubectl, sem instalação local. Dá trabalho manter, mas foi possível graças a um ex-membro do Google SRE. Querem continuar trabalhando assim. (As principais linguagens são Golang, Bash e Rust)
Com uma equipe minúscula de 1,5 pessoa, um repositório único é o natural. A experiência com Bazel foi péssima, mas talvez valha a pena em projetos grandes. Em equipes com menos de 2 pessoas, Kind+Tilt por si só já basta. A camada de ferramentas também é algo que Go já resolve em parte com
go.mod. O mesmo pode ser feito de forma parecida com kubectl. Também vale pensar no nível salarial de um ex-Googler. Espero que o custo de manutenção do Bazel continue valendo a pena.Na nossa empresa, fazemos deploy com serviços baseados em systemd e playbooks do ansible, e com tmuxinator inicializamos automaticamente em modo dev, de uma vez, backend/DB/motor de busca/frontend. Basta rodar o comando
tmuxinatorna raiz e todo o ambiente de desenvolvimento sobe na hora. Um monorepo único é esmagadoramente mais conveniente do que antes.Situação parecida aqui, com relato de enorme efeito positivo ao introduzir Bazel. Graças à camada de ferramentas, o ambiente de desenvolvimento se mantém consistente. No entanto, é preciso usar
bazel runmanualmente, e ficou a curiosidade sobre formas melhores de automação. Pedido para explicar como isso funciona.Em uma equipe de 2 pessoas, o próprio padrão de microservices/K8s já é overengineering. Nessa escala, qualquer abordagem funciona. Antigamente tudo rodava com Dropbox/SVN/MS VCS etc. (havia inconvenientes, claro), mas nada disso era realmente um problema. Nessa escala, todos conseguem manter o processo inteiro na cabeça. O relato é que ferramentas e infraestrutura complexas não são o fator de sucesso.
Relato de um freelancer que configurou monorepos três vezes em várias empresas ao longo dos últimos 4 anos. Como ficou restrito ao frontend e só usou o ecossistema JavaScript/TypeScript, isso tornou a gestão relativamente mais fácil. Um bom monorepo, na prática, funciona internamente como um polyrepo: cada projeto pode ser desenvolvido, implantado e hospedado de forma independente, mas coexistindo em uma única base de código, com compartilhamento livre de componentes comuns (UI etc.) e garantia de consistência visual. Como guia prático, recomenda este material.
No fim das contas, tudo depende do caso. Na nossa empresa, gerenciamos cerca de 40 repositórios git com CIs separados; depois de build/test/package, no final geramos uma imagem integrada de sistema de arquivos para testes de integração. Os componentes se comunicam por mensagens Flatbuffers, e o próprio flatbuffers também é gerenciado como submodule. Lidar com dependências downstream é difícil, mas alguma flexibilidade foi garantida com progressive enhancement. Numa situação assim, nem fica claro se isso é multirepo ou um monorepo cheio de submodules. Também não é evidente se haveria vantagens em mudar para monorepo. No fim, trata-se de trade-offs e de escolher que tipo de inconveniência você está disposto a tolerar.
Relato do autor do blog sobre ferramentas para monorepo. As pessoas costumam destacar só as vantagens do monorepo, mas na prática a complexidade de operar um monorepo com sucesso quase sempre é absorvida nos bastidores por times de devops/devtools. Por isso, a adoção deve ser cautelosa, embora, quando bem implementado, possa oferecer valor suficiente.
A experiência com um monorepo bem administrado é tão boa que dá vontade de nunca mais voltar para outro workflow. Mas a abordagem despreparada de “vamos de monorepo também” é um pesadelo. Se alguém vendesse um pacote com ambiente e ferramentas de monorepo já prontos, haveria uma grande oportunidade de negócio.
Pela experiência em grandes organizações, monorepo pode até restringir drasticamente as dependências entre times e reduzir o reuso de código. Quando um time de bibliotecas quer mudar algo, todos os usuários abaixo precisam ser atualizados, e isso se complica porque outros times usam de formas inesperadas (Hyrum's Law). No fim, grandes empresas acabam caindo em copiar e colar internamente, forks, controle de acesso rígido e aprovação lenta de mudanças.
Ao criar uma biblioteca para uso geral, é preciso ter muito cuidado no design da API. Se possível, o ideal é não mudar a API; se for preciso mudar, recomenda-se planejar bem a mudança em larga escala ou substituir por uma nova função e marcar a versão antiga como deprecated. Para código pequeno, copiar e colar também pode ser aceitável.
Ainda assim, a vantagem do monorepo é conseguir encontrar facilmente todos os usos e, quando necessário, alterá-los/corrigi-los de forma atômica.
Todo software precisa considerar dependências, e o monorepo, pelo contrário, aumenta o poder tanto de quem mantém a biblioteca quanto de quem a utiliza para modificar um ao outro.
Em monorepo, como é fácil adaptar o código à própria necessidade, a chance de reutilização é maior do que em polyrepo.