A conversa sobre arquitetura costuma ser apresentada como binária. De um lado o monólito, chamado de legado com desprezo. Do outro os microserviços, apresentados como o que empresa séria faz.
O que quase nunca entra na conversa é a opção do meio, que é onde a maioria dos produtos deveria viver por muitos anos: um processo só, um banco só, com fronteiras internas levadas a sério.
O que torna um monólito ruim
Não é ser um processo. É não ter fronteira.
O monólito que dá medo é aquele em que o serviço de cobrança faz SELECT na tabela de usuário, o de relatório importa uma classe do de pedido, e mudar uma coluna quebra três coisas que ninguém relacionava. O problema não é o deploy único: é que nada pode ser entendido isoladamente.
Microserviço resolve isso por força bruta. A fronteira passa a ser a rede, e a rede não deixa você fazer join na tabela do outro time.
Mas a rede cobra caro por essa disciplina: latência, falha parcial, transação distribuída, versionamento de contrato, observabilidade distribuída, e a operação de N serviços em vez de um.
Monólito modular pega a disciplina sem pagar a rede.
Como fica na prática
Módulos com fronteira explícita e uma regra que o código consegue verificar.
src/
billing/
index.ts <- a única porta de entrada do módulo
billing.service.ts
entities/
internal/ <- ninguém de fora importa daqui
orders/
index.ts
...
shared/
A regra: um módulo só importa de outro através do index.ts. Nada de importar arquivo interno, nada de importar entidade de outro módulo.
E isso não é combinado de code review, é verificado. Com ESLint:
// eslint.config.js
"no-restricted-imports": ["error", {
patterns: [{
group: ["**/billing/internal/*", "**/orders/internal/*"],
message: "Importe pela API pública do módulo (index.ts).",
}],
}],
Regra que a máquina verifica sobrevive à pressa. Combinado escrito na wiki não sobrevive à sexta-feira.
O banco compartilhado, com disciplina
Aqui mora a maior discordância, e minha posição é clara: um banco só, com regra de propriedade.
Cada tabela pertence a um módulo. Só esse módulo lê e escreve nela. Outro módulo que precisar do dado chama o serviço, não a tabela.
Isso te dá o que importa dos microserviços, que é o encapsulamento do modelo de dados, e mantém o que os microserviços te tiram, que é a transação. Poder fazer o pedido e o lançamento no ledger no mesmo commit é uma vantagem enorme, e quem foi pra microserviço cedo demais gastou meses reconstruindo isso com saga e compensação.
Se quiser ser rigoroso, dá pra usar schema separado por módulo no Postgres e permissão de leitura restrita. Aí a fronteira é o banco quem garante.
Comunicação entre módulos
Duas formas, e a escolha entre elas é o desenho de verdade.
Chamada direta pra leitura e pra operação que precisa de resposta imediata. É uma chamada de função. Rápida, com stack trace inteiro, depurável.
Evento pra tudo que é reação. Pedido criado dispara e-mail, atualiza métrica, avisa parceiro. O módulo de pedido não deveria conhecer nenhum desses.
// dentro da transação, junto com o dado
await this.events.emit("order.created", { orderId: order.id });
Emitir dentro da mesma transação e publicar depois é o outbox, e é o que impede evento perdido.
O ganho estrutural: no dia em que um módulo precisar virar serviço de verdade, ele já se comunica por evento e já tem porta de entrada definida. A extração vira trabalho de infraestrutura, não de arqueologia.
Quando vale quebrar
Existem razões legítimas, e todas elas são operacionais, não estéticas.
Escala independente: um módulo consome dez vezes mais CPU que o resto e você quer escalar só ele.
Tecnologia diferente: aquela parte precisa de outra linguagem por um motivo real.
Time e ritmo: times separados que se atrapalham no mesmo deploy. Essa é a razão mais comum de verdade, e ela aparece com bem mais gente do que a maioria dos times tem.
Isolamento de falha: aquele componente pode cair sem derrubar o resto.
Nenhuma dessas razões é “microserviço é mais moderno”. E nenhuma delas costuma existir num time de menos de vinte pessoas.
O custo real de errar pra cada lado
Errar pro monólito e ter que quebrar depois é trabalho, e é trabalho conhecido, incremental, com o sistema funcionando o tempo todo.
Errar pro microserviço cedo é diferente. Você paga o custo operacional todo dia, com um time que não tem gente sobrando pra operar aquilo, e a reversão é rara porque ninguém aprova um projeto chamado “juntar de volta”.
Assimetria de risco decide. Comece junto com fronteira clara. Quebre quando doer, pelo motivo certo, e você vai saber qual pedaço tirar porque a fronteira já está desenhada.