Por que dividir um modular monolith por camada (controllers, services, repositories) sai caro a longo prazo — e como desenhar fronteiras por capacidade de negócio, com Common Closure Principle, lei de Conway e medição via git history.
Dividir um modular monolith por camada (controllers, services, repositories) parece arrumado e custa caro. A fronteira que sobrevive ao tempo é por capacidade de negócio: o que muda junto, vive junto. O nome formal é Common Closure Principle, de Robert C. Martin (2002). A lei de Conway, de 1968, diz que o desenho do sistema espelha o desenho do time. Junte os dois e o git já te conta onde as fronteiras reais do seu código estão hoje.
A maior parte dos times que conheci, quando o monolito começa a doer, pula direto para microserviços. Eu já passei por isso três vezes. Nas três, o problema não era o monolito. Era o desenho de fronteiras dentro dele.
Modular monolith é a palavra da moda. Mas a palavra esconde o que de fato importa: onde você desenha a linha entre uma coisa e outra. E essa decisão sobrevive a qualquer escolha de framework.
O que é um modular monolith?
Um modular monolith é uma aplicação que continua sendo deployada como um único processo, mas internamente é cortada em módulos com fronteiras explícitas. Cada módulo tem seu próprio domínio, código e às vezes seu próprio schema de banco. A simplicidade operacional do monolito tradicional fica, sem o acoplamento entre features.
A diferença prática contra as outras formas comuns:
| Característica |
Monolito clássico |
Modular monolith |
Microsserviços |
| Unidade de deploy |
1 processo |
1 processo |
N processos |
| Fronteira entre módulos |
Convenção |
Pacote ou módulo explícito |
Rede (HTTP, gRPC) |
| Banco de dados |
1 schema compartilhado |
1 banco, schemas por módulo |
1 banco por serviço |
| Comunicação |
Chamada direta em memória |
Chamada direta via interfaces |
RPC síncrono ou eventos assíncronos |
| Custo operacional |
Baixo |
Baixo |
Alto (orquestração, observabilidade, falhas distribuídas) |
| Refatorar fronteira |
Doloroso (acoplamento implícito) |
Mecânico (mover pasta) |
Caro (migração de dados, deploy coordenado) |
| Tamanho de time |
1 a 3 times |
3 a 10 times |
10+ times com domínios independentes |
A maior parte do que segue neste texto é sobre a coluna do meio: como desenhar a fronteira interna que faz o modular monolith sustentar a si próprio por anos.
A pergunta que ninguém faz cedo
Quando um time decide criar um módulo novo, a conversa quase sempre começa por substantivos. "Vamos fazer o módulo de produtos." "Aqui entra o módulo de pedidos." "Falta o módulo de usuários."
Substantivo é fácil. Substantivo cabe no diagrama. O problema é que substantivo não diz nada sobre o que muda junto.
A pergunta útil é outra: quando algo mudar nesse domínio, quem precisa mudar com ele? Se a resposta for "três módulos diferentes em três PRs separados", a fronteira está errada. Você desenhou uma linha onde não existe.
Isso tem nome (e tem desde 2002)
Quando finalmente parei pra estudar por que esse padrão funcionava, descobri que Robert C. Martin já tinha dado nome em 2002, no livro Agile Software Development: Principles, Patterns, and Practices. Ele chamou de Common Closure Principle: as classes que mudam juntas, pelos mesmos motivos, devem viver juntas. As que mudam por motivos diferentes, devem viver separadas.
Ou seja: a fronteira boa não é a que parece bonita no diagrama. É a que minimiza o número de módulos tocados por uma mudança de negócio.
Vale juntar com a lei de Conway, de 1968: o desenho de um sistema espelha o desenho da organização que o produz. Se o seu time é dividido por capacidade (pagamentos, catálogo, busca), o código sustentável é dividido por capacidade também. Quando o time muda, o desenho precisa mudar junto. Quando o desenho não acompanha, dói.
Nada nesses dois textos é novo. O que é novo é quantos times de 2025 ainda organizam código como se essas duas ideias não existissem.
Um caso que me marcou
Há alguns anos peguei um sistema de checkout que tinha sido cortado por camada. A árvore raiz parecia isto:
src/
controllers/
CheckoutController.ts
PaymentController.ts
OrderController.ts
ShippingController.ts
services/
CheckoutService.ts
PaymentService.ts
OrderService.ts
ShippingService.ts
repositories/
CheckoutRepository.ts
PaymentRepository.ts
OrderRepository.ts
dtos/
...
entities/
...
Cada pasta com uma centena de arquivos por feature. Lindo na descrição. Doloroso na prática.
Adicionar um campo no formulário de pagamento exigia tocar oito arquivos espalhados por sete pastas: um DTO, uma entity, um repository (e migration), um service, um controller, dois testes, e um arquivo de validação central. O time já tinha automatizado parte com snippets do editor. Acharam que era produtividade. Era anestesia.
Quando reorganizei pelo que muda junto, sobrou isto:
src/
checkout/
http/
domain/
persistence/
tests/
payment/
http/
domain/
persistence/
tests/
shipping/
http/
domain/
persistence/
tests/
shared/
logging/
db/
http-kernel/
Cada pasta de cima é uma capacidade de negócio. Dentro dela, tudo que faz aquela capacidade existir. Adicionar o tal campo agora tocava dois arquivos, no mesmo diretório. Code review parou de exigir aba dupla no editor.
Não inventei nada. Só parei de organizar pelo que se parece e comecei a organizar pelo que muda junto.
Como medir antes de refatorar
A parte que falta na maioria das conversas sobre boundaries é que a fronteira certa pode ser descoberta com dado, não só com intuição. Você já tem o dado: está no git.
A heurística mais simples: arquivos que mudam no mesmo commit, repetidamente, ao longo de meses, são coesos. O git sabe disso. Você só precisa perguntar.
# Arquivos mais alterados nos últimos 6 meses,
# ordenados por número de commits que os tocaram.
git log --since=6.months.ago --name-only --pretty=format: \
| awk 'NF' \
| sort | uniq -c | sort -rn | head -30
Já te diz quais arquivos são quentes. Para achar pares acoplados (a parte que importa), o jeito honesto é olhar commit a commit. Algo nesta linha resolve:
# Lista, para cada commit dos últimos 6 meses, os arquivos
# tocados. Bom para grep manual de pares suspeitos.
git log --since=6.months.ago --name-only --pretty=format:'---%H' \
> /tmp/commits.txt
Para algo mais sério vale o code-maat, do Adam Tornhill. É a ferramenta dos livros Your Code as a Crime Scene e Software Design X-Rays. Ela calcula temporal coupling: a probabilidade de dois arquivos mudarem juntos. Resultado em CSV, fácil de ranquear. Pares com 80%+ de acoplamento temporal entre módulos diferentes são a evidência mais barata que você vai ter de fronteira errada.
# Exemplo simplificado de uso (após gerar o log no formato dele).
maat -l logfile.log -c git2 -a coupling \
| sort -t, -k3 -rn | head -20
Eu uso isso antes de qualquer reescrita grande. Em meia hora, o repositório te conta onde estão as fronteiras reais do sistema. Refatorar fica honesto: você está consertando algo que o histórico já provou estar quebrado.
Por que separar por camada parece certo
Eu entendo a tentação. Separar por camada é simétrico, ensina arquitetura limpa em uma olhada, e dá a sensação reconfortante de que cada arquivo tem seu lugar.
O problema é que a simetria é visual, não comportamental. Você ganha um diagrama bonito e perde a coesão. Um time que separa por camada vai, no longo prazo, abrir PRs cada vez maiores, porque toda mudança real cruza fronteiras horizontais.
E PR grande não é só lento. PR grande é onde bug se esconde.
Três perguntas antes de criar módulo novo
Quando eu desenho ou revisito uma fronteira, passo por três perguntas. Nessa ordem.
Quem é o dono dessa decisão? Se mais de um time toma decisão sobre o mesmo módulo, a fronteira está no lugar errado, ou o time está. Um dos dois vai mudar.
Quem precisa saber quando isso mudar? Se a resposta é "todo mundo no canal de engenharia", o módulo está cuidando de algo central demais para o tamanho dele. É hora de explicitar o contrato. Ou de quebrá-lo em duas coisas com nomes honestos.
Se eu apagar esse módulo amanhã, o que quebra? Resposta saudável: uma capacidade clara some, com sintomas previsíveis. Resposta ruim: dezesseis lugares param de funcionar por motivos diferentes. Isso é acoplamento escondido. A fronteira existe no diagrama, não no código.
Quando a regra não vale
Não funciona em tudo. Utilitários puros (formatação de data, parsing de moeda, helpers de string) não têm domínio próprio. Force eles dentro de um módulo e você só está adicionando indireção.
Código de plataforma também escapa. Logger, cliente de banco, instrumentação de tracing. Esse tipo de coisa atravessa todos os módulos por desenho. Tratá-lo como módulo de negócio só atrapalha. É o que vive em shared/ no exemplo acima, e quanto menos coisa aparecer lá, melhor.
Integrações de borda também merecem cuidado. Um webhook do Stripe não é "o módulo Stripe". Ele é parte do módulo que recebe pagamento, traduzido na borda. A fronteira fica entre o que é seu modelo e o que é o contrato deles.
O teste de seis meses
A parte mais útil dessa abordagem só aparece depois. Seis meses, um ano. Você abre o repositório para mexer em algo que não tocava há tempo, e a pergunta que vale é simples: ainda dói?
Se dói, a fronteira drenou. Provavelmente porque o negócio mudou e ninguém atualizou o desenho. Acontece. Refatorar fronteira é trabalho normal de manutenção, não falha do passado. Rode o git log de novo e veja onde o acoplamento se mexeu.
Se não dói, você fez a coisa certa. Não porque adivinhou o futuro, mas porque desenhou pelo que muda junto, e isso continuou verdade depois que o time esqueceu por que tinha sido feito daquele jeito.
Modular monolith não é um padrão. É a consequência de levar essa pergunta a sério desde o primeiro commit.
Perguntas frequentes
Modular monolith é o mesmo que microsserviços?
Não. Microsserviços rodam em processos separados que se comunicam pela rede. Modular monolith roda em um único processo. A semelhança é só interna: ambos forçam fronteiras explícitas entre capacidades. O custo operacional é completamente diferente.
Quando faz sentido migrar de modular monolith pra microsserviços?
Quando uma capacidade tem necessidade real de escala independente, ciclo de deploy distinto, ou time dedicado. Não migre por moda. Cada microsserviço carrega custo permanente de observabilidade, deploy e falhas em cascata. Esse custo só compensa quando a capacidade não cabe mais no monolito.
Como começar a reorganizar um monolito existente por capacidade?
Comece medindo. Rode git log sobre os últimos seis meses e ranqueie arquivos que mudam juntos com frequência. Esses pares apontam capacidades implícitas que a arquitetura atual está cortando ao meio. Use code-maat se quiser ir além do git log manual. Reorganize um módulo de cada vez, em PRs menores que 500 linhas.
Sim. Não é um padrão de framework, é um padrão de organização. Os exemplos deste artigo usam TypeScript, mas a mesma divisão por capacidade funciona em Go, Java, .NET, Python ou Ruby. O que muda é a ferramenta que enforça a fronteira (pacotes, módulos, namespaces), não a regra.
Para continuar lendo
Estes são os textos que eu cito quando alguém quer aprofundar. Todos sustentam, de algum ângulo, a tese de cima.
- Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices (Prentice Hall, 2002), capítulos sobre Package Cohesion Principles. Reformalizado em Clean Architecture (2017), capítulo "Component Cohesion". O nome formal do que o artigo defende.
- Melvin Conway, How Do Committees Invent? (1968). O artigo original da lei de Conway, ainda surpreendentemente curto.
- Martin Fowler, MonolithFirst (2015). Por que microserviços primeiro quase sempre é prematuro.
- Adam Tornhill, Your Code as a Crime Scene (2024, 2ª edição). Tudo que dá pra descobrir lendo git history em vez de adivinhando.
- Kirsten Westeinde, Deconstructing the Monolith (Shopify Engineering, 2019). Como um dos maiores monolitos em produção foi reorganizado por capacidade, não por camada.
- DHH, The Majestic Monolith (2016). A defesa curta e honesta do monolito bem desenhado.
- Kamil Grzybek, Modular Monolith: A Primer (2019). Série técnica em .NET, mas com ideias agnósticas de stack.
Enable JavaScript for the full experience.