O que é um monorepo e por que faz sentido para Go
Um monorepo (repositório monolítico) e uma abordagem onde múltiplos projetos, serviços ou módulos vivem no mesmo repositório Git - ao contrario da estratégia tradicional de multi-repo, onde cada serviço tem seu próprio repositório separado. Empresas como Google, Meta e Uber usam monorepos gigantescos ha décadas, e a discussão sobre quando adotar essa abordagem voltou com força para times de Go que gerenciam microsservicos.
Para equipes com 3 a 15 microsservicos em Go, o multi-repo frequentemente cria mais problemas do que resolve: dependências cruzadas versionadas de forma inconsistente, pipelines de CI/CD duplicados em cada repositório, dificuldade de fazer mudanças atómicas que afetam vários serviços ao mesmo tempo e overhead de gerenciar N repositórios com configurações parecidas mas nunca idênticas.
O monorepo não elimina a separação lógica dos serviços - cada microsservico continua sendo um módulo Go independente com seu próprio binário. O que muda e onde o código vive e como o pipeline de build e release e gerenciado.
Monorepo faz mais sentido quando seus serviços compartilham código (pacotes utilitários, structs de domínio, clientes HTTP internos) ou quando mudanças em um serviço frequentemente exigem mudanças coordenadas em outros.
Como funciona a automação de releases em monorepo
O segredo da eficiência em um monorepo Go e o conceito de builds afetados: em vez de buildar e deployar todos os serviços a cada commit, o pipeline identifica quais serviços foram realmente modificados e faz o release apenas deles.
Essa detecção e feita analisando quais arquivos foram alterados no commit ou PR (via git diff) e mapeando esses arquivos para os módulos Go afetados. Se um commit altera apenas services/pagamentos/, apenas o serviço de pagamentos passa pelo pipeline completo de build, teste e deploy. Os demais são ignorados naquela rodada.
Quando um pacote compartilhado muda (por exemplo, um cliente HTTP utilitário em pkg/httpclient/), o pipeline precisa identificar todos os serviços que importam esse pacote e buildá-los todos - garantindo que nenhum serviço fica com uma versão desatualizada da dependência interna.
Principais vantagens desta abordagem
Os ganhos concretos de migrar para monorepo com automação de releases são:
- Pipeline unificado: um único arquivo de CI/CD (GitHub Actions, GitLab CI ou similar) cobre todos os serviços, com lógica condicional de quais etapas rodar.
- Mudanças atómicas: um único PR pode alterar o contrato de uma API interna E o cliente que a consome, garantindo consistência antes do merge.
- Visibilidade de dependências: fica fácil ver quais serviços dependem de quais pacotes, sem precisar rastrear manualmente entre repositórios.
- Onboarding simplificado: novo dev clona um repositório e tem acesso a todo o código do sistema.
- Tags de release por serviço: o pipeline cria tags no formato
serviço/v1.2.3, permitindo versionar cada serviço independentemente mesmo dentro do monorepo.
Como começar: estrutura de pastas e configuração
A estrutura recomendada para um monorepo Go com microsservicos e:
monorepo/
services/
pagamentos/
main.go
go.mod # módulo Go independente
go.sum
Dockerfile
pedidos/
main.go
go.mod
go.sum
Dockerfile
notificações/
main.go
go.mod
go.sum
Dockerfile
pkg/
httpclient/ # pacote compartilhado
client.go
go.mod
logger/ # outro pacote compartilhado
logger.go
go.mod
.GitHub/
workflows/
release.yml # pipeline unificado
Makefile # comandos utilitáriosCada serviço tem seu próprio go.mod, o que significa que são módulos Go separados. Os pacotes em pkg/ também tem seus próprios go.mod e são referenciados via replace no go.mod dos serviços durante desenvolvimento local:
# services/pagamentos/go.mod
module GitHub.com/empresa/monorepo/services/pagamentos
go 1.22
require (
GitHub.com/empresa/monorepo/pkg/httpclient v0.3.1
GitHub.com/empresa/monorepo/pkg/logger v0.2.0
)
# Para desenvolvimento local, usar replace:
replace GitHub.com/empresa/monorepo/pkg/httpclient => ../../pkg/httpclientExemplo prático: detectando serviços afetados no GitHub Actions
O coração da automação e o script que detecta quais serviços foram modificados. Aqui esta um exemplo funcional em shell para usar no GitHub Actions:
# .GitHub/workflows/release.yml
name: Release Microsservicos
on:
push:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
services: ${{ steps.detect.outputs.services }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- name: Detectar serviços afetados
id: detect
run: |
CHANGED=$(git diff --name-only HEAD~1 HEAD)
SERVICES=""
for dir in services/*/; do
SERVICE=$(basename $dir)
# Checar se o serviço ou algum pacote compartilhado mudou
if echo "$CHANGED" | grep -q "^services/$SERVICE/"; then
SERVICES="$SERVICES $SERVICE"
fi
# Checar dependências em pkg/
for pkg in pkg/*/; do
PKG=$(basename $pkg)
if echo "$CHANGED" | grep -q "^pkg/$PKG/"; then
# Verificar se este serviço importa este pkg
if grep -q "monorepo/pkg/$PKG" "services/$SERVICE/go.mod"; then
SERVICES="$SERVICES $SERVICE"
fi
fi
done
done
# Remover duplicatas e formatar como JSON
UNIQUE=$(echo $SERVICES | tr ' ' '\n' | sort -u | jq -R . | jq -sc .)
echo "services=$UNIQUE" >> $GITHUB_OUTPUT
build-and-deploy:
needs: detect-changes
if: needs.detect-changes.outputs.services != '[]'
strategy:
matrix:
service: ${{ fromJson(needs.detect-changes.outputs.services) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build ${{ matrix.service }}
run: |
cd services/${{ matrix.service }}
go build ./...
go test ./...
- name: Deploy ${{ matrix.service }}
run: echo "Deploy do serviço ${{ matrix.service }}"Comparação com alternativas
Existem outras abordagens para gerenciar microsservicos Go além do monorepo:
- Multi-repo tradicional: cada serviço tem seu repositório. Funciona bem para times pequenos ou serviços verdadeiramente independentes, mas escala mal quando os serviços precisam se coordenar.
- Ferramentas de monorepo (Nx, Turborepo, Bazel): oferecem cache de builds e detecção de mudanças mais sofisticada, mas adicionam complexidade e tem suporte variável para Go.
- Git submodules/subtrees: tentativa de ter o melhor dos dois mundos, mas na prática são complexos de manter e propensos a erros.
O diferencial do monorepo Go nativo (sem ferramentas externas) e a simplicidade: você usa apenas Git, shell scripts e o sistema de módulos do próprio Go. Sem dependências extras, sem curva de aprendizado de uma nova ferramenta.
Use o comando go mod graph dentro de cada serviço para visualizar o grafo de dependências e identificar quais pacotes compartilhados afetam quais serviços. Isso facilita planejar a lógica de detecção de mudanças no CI/CD.
Pontos positivos e limitações
Pontos positivos:
- Pipeline unificado reduz drasticamente a duplicação de configuração de CI/CD.
- Mudanças que afetam vários serviços ficam em um único PR, facilitando code review e rastreabilidade.
- Refatorações de pacotes compartilhados são mais seguras - você ve imediatamente quais serviços quebram.
- Tags de release por serviço permitem versionar independentemente.
Limitações reais:
- Repositórios grandes ficam mais lentos para clonar - mitigável com
--depth 1no CI. - Conflitos de merge são mais frequentes quando o time e maior, pois todos trabalham no mesmo repositório.
- A detecção de dependências transativas (pkg A usa pkg B, serviço usa pkg A) precisa ser implementada explicitamente - o exemplo simples acima não cobre esse caso.
- Ferramentas de análise de código que assumem um único
go.modpor repositório podem se comportar de forma inesperada.
Casos de uso reais
Startup com 4 a 8 microsservicos: o monorepo reduz a sobrecarga administrativa de manter N pipelines diferentes. O time consegue fazer refatorações de domínio que afetam vários serviços em um único dia, sem coordenar PRs em repositórios diferentes.
Time de plataforma: gerência pacotes utilitários compartilhados (autenticação, logging, clientes de filas) consumidos por dezenas de serviços. No monorepo, uma atualização no pacote de autenticação dispara automaticamente o rebuild e reteste de todos os serviços consumidores.
Projeto open source com plugins: o core e os plugins vivem no mesmo repositório, facilitando contribuições que afetam ambos e garantindo que o core e os plugins estejam sempre sincronizados em versão.
Equipe que faz deploy frequente: o pipeline só roda para serviços afetados, então deploys de mudanças pequenas são rápidos - sem precisar buildar e testar toda a stack a cada commit.
Dicas e boas práticas
Adicione um Makefile na raiz do monorepo com targets como make test-all, make build SERVICE=pagamentos e make lint. Isso padroniza comandos entre todos os serviços e facilita o onboarding.
Use git log --oneline -- services/pagamentos/ para ver o histórico de commits de um serviço específico dentro do monorepo, sem precisar de ferramentas extras.
Durante a migração de multi-repo para monorepo, preserve o histórico Git de cada serviço usando git subtree add em vez de simplesmente copiar os arquivos. Perder o histórico torna git blame e arqueologia de bugs muito mais difícil.
Configure o GitHub branch protection para exigir que apenas os checks dos serviços afetados passem antes do merge - não todos os checks. Isso evita que um PR pequeno fique bloqueado por um serviço não relacionado com test flaky.
Vale a pena migrar para monorepo?
Para a maioria dos times brasileiros com 3 a 10 microsservicos Go, a resposta e sim. O custo da migração e um dia ou dois de trabalho (usando git subtree para preservar histórico), e os ganhos em produtividade aparecem rapidamente na forma de menos overhead de CI/CD e mais facilidade para refatorações.
Se você tem mais de 20 serviços ou um time grande (mais de 15 devs), considere ferramentas mais sofisticadas como Bazel ou Nx que oferecem cache de builds distribuído e detecção de mudanças mais robusta. Para times menores, a abordagem nativa com shell scripts no CI/CD e suficiente e muito mais simples de manter.
O próximo passo e auditar quais dos seus serviços atuais compartilham código - se a resposta for vários, o monorepo provavelmente vai economizar horas de trabalho por semana para o time.
Comentários
Deixar um comentárioVocê precisa ter uma conta no CuritibaBlog para comentar.