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.

💡
Dica

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ários

Cada 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/httpclient

Exemplo 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.

🚀
Pro tip

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 1 no 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.mod por 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

💡
Dica

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.

💡
Dica

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.

⚠️
Atenção

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.

🚀
Pro tip

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.