Blog

Monólito modular: a arquitetura que a maioria dos times deveria ter.

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.

← Voltar pro blog