Estruturas de Projeto

Monorepo vs Multi-repo

Decisão sobre onde o código vive: num único repositório Git que abriga múltiplos projetos e pacotes (monorepo), ou em repositórios separados, cada um com seu ciclo de vida independente (multi-repo). São estratégias ortogonais à arquitetura do software — e confundi-las com monolito é a armadilha mais comum.

Intenção

Definir a estratégia de organização de repositórios Git para múltiplos projetos, pacotes ou serviços de uma organização, equilibrando visibilidade e compartilhamento de código (monorepo) com isolamento e autonomia de ciclo de vida (multi-repo).

A escolha não é sobre arquitetura do sistema — é sobre onde o código fica versionado. Empresas como Google, Meta e Microsoft mantêm virtualmente todo o seu código num único repositório gigante. Outras preferem um repositório por serviço ou produto. Ambas as abordagens funcionam em escala; os trade-offs são diferentes.

Problema

À medida que um time cresce e o número de projetos ou pacotes aumenta, surgem tensões que qualquer estratégia de repositório precisa resolver:

  • Compartilhamento de código: como reutilizar um utilitário ou uma biblioteca interna sem duplicar ou publicar um pacote externo a cada mudança?
  • Coordenação de mudanças: quando uma alteração numa dependência compartilhada precisa refletir em múltiplos projetos ao mesmo tempo, como garantir consistência?
  • Autonomia de equipes: como permitir que times diferentes evoluam seus projetos em ritmos distintos sem travar uns aos outros?
  • Visibilidade: como descobrir o que já existe para não reinventar a roda?

Monorepo e multi-repo são respostas diferentes a essas tensões, e cada um as resolve melhor em contextos distintos.

Estrutura

Multi-repo

Cada projeto, serviço ou biblioteca vive num repositório Git separado. É o modelo padrão do GitHub open-source: cada projeto tem seu próprio ciclo de releases, CI/CD, permissões e histórico.

  GitHub / GitLab / Bitbucket
  │
  ├── org/api-gateway          ← repositório independente
  │     ├── src/
  │     ├── package.json       (version: "2.1.0")
  │     └── .github/workflows/
  │
  ├── org/user-service         ← repositório independente
  │     ├── src/
  │     ├── package.json       (version: "1.5.3")
  │     └── .github/workflows/
  │
  ├── org/shared-utils         ← biblioteca publicada no npm/registry
  │     ├── src/
  │     └── package.json       (version: "0.9.0", name: "@org/shared-utils")
  │
  └── org/frontend-app         ← repositório independente
        ├── src/
        │     └── package.json  (depends on "@org/shared-utils": "^0.9.0")
        └── .github/workflows/

Monorepo

Todos os projetos e pacotes convivem num único repositório Git. O histórico é compartilhado; mudanças que afetam múltiplos pacotes aparecem num único commit ou PR.

  org/monorepo                 ← único repositório Git
  │
  ├── apps/
  │     ├── api-gateway/       ← projeto: app Node/Express
  │     │     └── src/
  │     └── frontend/          ← projeto: app React/Vue
  │           └── src/
  │
  ├── packages/
  │     ├── shared-utils/      ← pacote interno compartilhado
  │     │     └── src/
  │     └── ui-components/     ← biblioteca de componentes
  │           └── src/
  │
  ├── package.json             ← workspace root (pnpm/yarn/npm workspaces)
  └── turbo.json               ← pipeline de build (Turborepo)

Variantes do monorepo

Três variantes principais se diferenciam pelo nível de tooling:

  • Monorepo puro (estilo Google/Meta): um repositório único para literalmente todo o código da empresa. Requer ferramentas proprietárias (Bazel, Pants, Buck) para que o CI só recompile e reteste o que mudou. A escala exige investimento pesado em tooling.
  • Monorepo com workspaces: o modelo mais comum fora do mundo das big techs. Ferramentas como Turborepo, Nx ou pnpm workspaces gerenciam as dependências entre pacotes internos, proveem cache incremental de build e permitem rodar tarefas apenas nos pacotes afetados por uma mudança.
  • Multi-repo com pacotes publicados: variante do multi-repo onde as bibliotecas compartilhadas são publicadas num registry privado (npm private, Verdaccio, GitHub Packages) e consumidas como dependências versionadas. Mantém o isolamento de repositórios mas adiciona o custo de ciclos de publicação para propagação de mudanças.

Como funciona

Compartilhamento de código no monorepo

Com workspaces, um pacote interno é referenciado diretamente pelo caminho local — sem precisar publicar no npm a cada mudança:

  // apps/api-gateway/package.json
  {
    "dependencies": {
      "@org/shared-utils": "workspace:*"
    }
  }

  // A ferramenta de workspace (pnpm, Turborepo, Nx) resolve
  // "@org/shared-utils" para packages/shared-utils/ localmente.
  // Nenhuma publicação é necessária para testar a mudança.

Cache incremental no monorepo

Ferramentas como Turborepo mantêm um grafo de dependências entre pacotes e um hash do conteúdo de cada um. Quando apenas shared-utils muda, somente os pacotes que dependem dele são recompilados e retestados. Sem essa camada de cache, o CI de um monorepo grande retesta tudo a cada push — tornando-o mais lento que o multi-repo equivalente.

Propagação de mudanças no multi-repo

No multi-repo, uma mudança numa biblioteca compartilhada requer: (1) commit e push na biblioteca, (2) publicação de uma nova versão no registry, (3) atualização do package.json em cada consumidor, (4) PR + CI em cada repositório consumidor. Para uma mudança de breaking change que afeta 10 serviços, isso gera 10 PRs separados que precisam ser coordenados manualmente.

Quando usar cada estratégia

Prefira monorepo quando:

  • O código é fortemente interdependente: mudanças frequentes cruzam múltiplos projetos ou pacotes. Um monorepo permite PR único, revisão unificada e CI que valida tudo de uma vez.
  • O time é pequeno a médio e trabalha nos mesmos projetos: a visibilidade total do código reduz silos e facilita refatorações transversais sem coordenação de repositórios.
  • Padronização é prioritária: configuração de linter, formatter, TypeScript e CI pode ser centralizada uma vez e aplicada a todos os projetos automaticamente.
  • Há muitas bibliotecas internas compartilhadas: o ciclo de desenvolvimento de libs internas sem a necessidade de publicação acelera dramaticamente a iteração.

Prefira multi-repo quando:

  • Equipes têm autonomia total e ciclos de release independentes: times que nunca ou raramente compartilham código e que precisam de permissões de acesso diferentes se beneficiam do isolamento do multi-repo.
  • Os projetos têm tecnologias muito distintas: um serviço em Rust, outro em Python e um frontend em TypeScript têm pouquíssimo a ganhar de um monorepo — o tooling compartilhado não se aplica.
  • Há pacotes que serão publicados como open-source: bibliotecas destinadas à comunidade são mais naturalmente gerenciadas em repositórios próprios, com seus próprios issues, releases e mantenedores.
  • O time não quer investir em tooling de monorepo: sem cache incremental, um monorepo grande punirá o CI com builds lentos. Se não há disposição para manter Turborepo, Nx ou similar, o multi-repo é mais simples de operar.

Prós e contras

Monorepo — Prós

  • Mudanças atômicas: uma alteração que afeta múltiplos pacotes aparece num único commit e PR.
  • Sem necessidade de publicação para iterar em bibliotecas internas — o workspace resolve localmente.
  • Padronização de tooling, linting e CI centralizada num único lugar.
  • Visibilidade total do código — qualquer desenvolvedor pode descobrir e reutilizar o que existe.
  • Refatorações transversais (rename, assinatura de interface) são feitas em um único PR.

Monorepo — Contras

  • CI mais lento sem cache incremental — sem tooling adequado, tudo é retestado a cada push.
  • Repositório cresce indefinidamente; git clone e operações de histórico ficam pesados sem ferramentas como git sparse-checkout.
  • Permissões de acesso são por repositório no Git — controle granular de quem acessa o quê exige workarounds (CODEOWNERS, branch protection).
  • Acoplamento acidental é mais fácil: a barreira de fazer um import entre projetos é zero.

Multi-repo — Prós

  • Isolamento total: um repositório não pode afetar outro por acidente.
  • Permissões de acesso nativas por repositório.
  • CI de cada repositório é simples e rápido — testa apenas o que está naquele repo.
  • Ciclos de release completamente independentes.

Multi-repo — Contras

  • Propagação de mudanças em libs compartilhadas exige múltiplos PRs coordenados manualmente.
  • Duplicação de configuração (linter, CI, tsconfig) em cada repositório — divergências surgem com o tempo.
  • Dificuldade de descoberta: o que já existe e pode ser reutilizado?
  • Versioning hell: dependências internas com versões defasadas coexistindo em serviços diferentes.

Armadilhas comuns

1. Confundir monorepo com monolito

Esta é a confusão mais frequente — e mais custosa. Monorepo e monolito são eixos ortogonais. Monorepo é uma estratégia de versionamento de código; monolito é uma decisão de arquitetura de deployment.

As quatro combinações são completamente válidas:

  • Monolito em monorepo: uma única aplicação deployada como um bloco, com todo o código num único repositório.
  • Microsserviços em monorepo: múltiplos serviços independentemente deployáveis, todos versionados juntos (o modelo do Google).
  • Monolito em multi-repo: uma única aplicação deployada como bloco, com seu código em repositório próprio.
  • Microsserviços em multi-repo: cada serviço tem seu repositório independente (o modelo mais comum em adoções iniciais de microsserviços).

Atenção: mover de multi-repo para monorepo não distribui automaticamente um sistema monolítico. E adotar microsserviços não obriga multi-repo. São decisões independentes.

2. Monorepo sem cache incremental

Montar um monorepo juntando os projetos num único repositório sem configurar cache incremental de build e teste é a receita para um CI que cresce linearmente com o tamanho do repositório. Antes de migrar para monorepo, defina qual ferramenta de orquestração será usada (Turborepo, Nx, Bazel) e configure o pipeline incremental desde o início.

3. Acoplamento acidental via imports internos

No monorepo, a barreira técnica para importar de outro pacote é zero — basta o caminho relativo funcionar. Sem enforcement de boundaries (regras de lint como eslint-plugin-boundaries ou as restrições de projeto do Nx), equipes inadvertidamente criam dependências entre pacotes que deveriam ser independentes. O resultado é um monorepo onde nada pode ser deployado isoladamente — o pior dos dois mundos.

4. Histórico gigante sem sparse-checkout

Em monorepos com anos de histórico e muitos arquivos binários, o git clone pode levar minutos. A solução é usar git clone --filter=blob:none --depth=1 (shallow + partial clone) ou configurar sparse-checkout para que cada desenvolvedor obtenha apenas os diretórios que precisa.

Tópicos relacionados

A decisão de monorepo vs multi-repo influencia como as equipes organizam o código internamente, mas não determina essa organização. Um monorepo com múltiplos projetos pode ter cada projeto organizado por feature ou por camada técnica — são decisões independentes que se complementam.

A distinção entre monolito e microsserviços é ortogonal à estratégia de repositório: a arquitetura de deployment define como os componentes rodam em produção, enquanto a estratégia de repositório define como o código é versionado e desenvolvido. Organizações que adotam microsserviços frequentemente migram para monorepo justamente para recuperar a visibilidade e a coordenação de mudanças que o multi-repo por serviço dificulta.