Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

01 - Monolito (Monolithic)

video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

O monolito é o estilo arquitetural em que toda a aplicação vive em uma única base de código, empacotada em um único artefato e executada como um processo, ou como réplicas idênticas desse processo. Os módulos internos se comunicam por chamada de função na memória, compartilham a mesma conexão de banco e participam da mesma transação sem nenhum mecanismo distribuído no caminho. Apesar da associação com sistemas legados, o monolito é o ponto de partida racional para a maioria dos projetos e a escolha definitiva de sistemas de altíssimo tráfego. Stack Overflow, Shopify e Basecamp operaram por muitos anos monolitos que atendem milhões de requisições por dia com equipes enxutas. A pergunta certa não é se o estilo é moderno, e sim se o acoplamento interno está sob controle.

Conceito e Funcionamento Interno

A mecânica é direta: uma codebase, um artefato, um pipeline e um processo. Quando o módulo de pedidos precisa do módulo de clientes, a chamada é um método local, sem serialização, sem latência de rede e sem timeout para gerenciar. Se a operação precisa tocar duas tabelas, ambas participam da mesma transação ACID, e o rollback é atômico por natureza. Observabilidade também é simples em comparação com ambientes distribuídos: um stack trace completo atravessa toda a lógica de negócio, e um único log concentra a execução.

O modelo de escala é a réplica: executa-se o mesmo artefato em N instâncias atrás de um balanceador, e o banco escala à parte com réplicas de leitura e tuning. Não há como escalar só a parte quente do sistema dentro do monolito; um relatório pesado concorre por CPU com o checkout na mesma JVM ou no mesmo processo. Essa limitação não é bug, é o preço da simplicidade, e passa a doer apenas quando os perfis de carga se tornam muito assimétricos.

A unidade de implantação concentrada define a rotina operacional do dia a dia:

  • Um artefato versionado: a tag de release descreve o sistema inteiro, e a pergunta de qual versão está rodando tem resposta única.
  • Um pipeline: build, testes e deploy seguem um único caminho, mais fácil de auditar, otimizar e reproduzir.
  • Um runtime afinado: parâmetros de JVM, workers e pools são compartilhados por todas as funcionalidades, o que simplifica tuning e complica isolamento.
  • Um banco coabitado: migrations de módulos diferentes tocam o mesmo esquema, exigindo disciplina explícita de tabelas por módulo.

O Problema que Resolve

Para equipes pequenas e produtos em descoberta, a distribuição é um custo alto com retorno incerto, e o monolito elimina de uma vez toda uma família de problemas que os microsserviços resolvem depois com ferramentas caras. A velocidade de iteração é o ganho mais visível: compilar, rodar e testar localmente o sistema inteiro é trivial, e um refatoramento que cruza cinco módulos é validado pelo compilador em segundos.

Fríções que simplesmente não existem no monolito bem organizado:

  • Falhas de rede entre módulos: chamada local não tem particionamento, retry nem latência variável; o pior caso é uma exceção previsível.
  • Compatibilidade de contratos: não há versão de API entre partes do sistema, porque não há limite de processo entre elas.
  • Consistência entre operações: uma transação local cobre pedidos, estoque e faturamento com garantia ACID e sem padrão saga.
  • Tracing distribuído: o fluxo completo cabe em um stack trace e em um identificador de requisição simples.
  • Pipeline de entrega: um único build, um único teste de integração e um rollback que devolve o sistema inteiro de uma vez.

Acoplamento Interno e Degeneração

A degeneração clássica não vem do estilo, e sim da ausência de fronteiras impostas. Sem regras testadas de dependência, o módulo de utilidades vira depósito de tudo, surgem ciclos de importação, e qualquer classe passa a poder chamar qualquer outra. O resultado é a bola de lama com monolito por fora: testes demoram horas, o build cresce sem motivo e cada deploy vira evento tenso, porque ninguém sabe o que mais aquela mudança toca. Com o tempo, a equipe responde com release trains espaçadas, que aumentam o lote de cada deploy e pioram o diagnóstico, num ciclo que destrói a confiança na entrega.

A lei de Conway amplifica o efeito: um sistema mantido por um único time tende a manter coerência interna, e o monolito combina bem com essa configuração. Dividir o time sem dividir o sistema, ou dividir o sistema sem redestruir as responsabilidades dos times, produz atrito imediato. O diagnóstico correto para um monolito sofrível quase nunca é "virar microsserviços", e sim "recuperar modularidade".

Sintomas que indicam degeneração em curso:

  • Ciclos de dependência entre módulos: a extração futura de qualquer parte puxa o resto do sistema junto.
  • Suíte de testes acima de alguns minutos: o custo de validação desestimula testes e empurra verificação para produção.
  • Mudanças locais com efeitos distantes: qualquer correção simples gera regressão em módulo aparentemente sem relação.
  • Medo de deploy: janelas de manutenção, checklist manual e rollout em madrugada como rotina, não como exceção.
  • Classes utilitárias gigantes: o ponto de acoplamento universal que tudo importa e que tudo importa de volta.

Monolito Modular

A resposta madura é o monolito modular: as fronteiras internas são definidas, documentadas e verificadas por ferramenta, mantendo o deploy atômico. Módulos expõem apenas uma API pública, não acessam tabelas alheias, comunicam-se por interfaces e por eventos internos, e a violação dessas regras quebra o build por meio de testes de dependência como ArchUnit, NetArchTest ou import-linter. Essa disciplina preserva a velocidade do monolito e prepara a extração futura, porque um módulo isolado de verdade é o único candidato honesto a virar serviço.

app/
  modulo-pedidos/
    api/            # contratos públicos do módulo; única porta de entrada
    interno/        # regras de negócio; nenhum outro módulo importa daqui
    infra/          # repositórios e adaptadores do módulo
    schema.sql      # o módulo é dono exclusivo das tabelas que usa
  modulo-estoque/
    api/
    interno/
    infra/
    schema.sql
  integracoes/
    eventos/        # contrato interno de eventos entre módulos

Com essa estrutura, a auditoria de dependência é mecânica: se o módulo de estoque precisar de algo de pedidos, ele consome a porta api, nunca interno. O passo seguinte, quando houver gatilho real, é extrair módulo e schema juntos, porque a fronteira de dado já existe. Times que pulam direto para microsserviços sem esse treino interno costumam recriar o acoplamento em rede, com custo maior.

# Evento interno entre módulos, publicado na mesma transação do banco
evento:
  nome: PedidoConfirmado         # fato no passado, contrato estável
  produtor: modulo-pedidos
  consumidores:
    - modulo-estoque             # baixa a reserva do item
    - modulo-faturamento         # emite a nota fiscal do pedido
  entrega: em processo, com tabela de eventos e reentrega em falha

O evento interno mantém os módulos desacoplados em tempo, mesmo dentro do processo: o publicador não conhece os consumidores, e um consumidor lento não trava o confirmar do pedido. Quando o módulo for extraído, o mesmo contrato migra para o broker com pouca mudança, porque a semântica já estava correta.

Vantagens em Equipes Pequenas

  • Deploy atômico: o sistema sobe como um todo, coerente em versão; rollback devolve tudo de uma vez, sem matriz de compatibilidade.
  • Transações simples: garantia ACID local atravessa recursos sem saga, sem idempotência de consumidor e sem conciliação noturna.
  • Operação enxuta: um processo para monitorar, um log para varrer, um perfil de JVM ou runtime para ajustar; custo de infraestrutura proporcionalmente menor.
  • Refatoração assistida: IDE e compilador enxergam o sistema inteiro, então renomear, extrair e mover código é seguro e rápido.
  • Onboarding direto: a pessoa nova lê o fluxo completo de ponta a ponta, sem pular entre repositórios e ambientes.
  • Debugging honesto: reproduzir um bug localmente reproduz a produção, sem orquestrar dez serviços no notebook.

Como Escalar um Monolito em Produção

Antes de distribuir, esgote as alavancas de escala vertical e de arquitetura interna, porque elas resolvem a maioria dos gargalos reais. Cache em camadas, do in-process ao Redis, ataca leitura repetida; réplicas de leitura distribuem consultas pesadas; filas internas transformam trabalho urgente em trabalho assíncrono; e tuning de banco, índices e consultas costuma devolver ordens de magnitude de desempenho. Escala horizontal por réplica cobre crescimento de tráfego enquanto o estado ficar no banco e na cache, não na memória do processo.

  • Replicar o processo web: instâncias stateless atrás de load balancer, com auto scaling por CPU ou latência.
  • Separar leitura e escrita no banco: consultas analíticas e telas de listagem vão para réplicas, writes ficam no primário.
  • Cache de agregações caras: invalidação por evento interno em vez de TTL cego, quando a corretude importa.
  • Assíncronizar o que não é urgente: e-mail, geração de PDF e integrações externas saem do caminho crítico do request.
  • Isolar carga pesada em processos separados: workers do mesmo artefato consumindo filas, sem ainda virar serviço distinto.

Há um teto honesto nesse caminho: quando módulos precisam de ciclos de release radicalmente diferentes, ou quando a distância de carga justifica tecnologias distintas, a extração passa a valer o custo. A decisão fica mais segura quando o módulo alvo já é modular, tem schema próprio e dono declarado, porque o trabalho restante é de infraestrutura e contrato, e não de redesenho de regras.

Casos Reais de Sucesso

O Stack Overflow opera, essencialmente, um monolito em .NET que está entre os sites mais acessados do mundo, servindo bilhões de impressions mensais com um punhado de servidores físicos, caching agressivo e um SQL Server bem afinado. O caso demonstra que tráfego, por si só, não exige distribuição; exige engenharia de desempenho no lugar certo. A equipe conhece cada caminho quente e otimiza com medição, e não com subdivisão de processos.

A Shopify mantém um monolito em Rails trabalhado por milhares de desenvolvedores, sustentado por um programa explícito de modularização, com componentes de propriedade declarada e ferramentas que impedem o retorno ao acoplamento. A 37signals, do Basecamp, reforça deliberadamente o monolito e o Rails para manter uma equipe pequena altamente produtiva, e reduziu custos drasticamente ao sair da nuvem em favor de servidores próprios. O Instagram dos primeiros anos escalou de milhões para dezenas de milhões de usuários com um monolito Django mantido por pouco mais de uma dúzia de engenheiros. O padrão comum é claro: o gargalo quase sempre mora no banco e no I/O, e a distribuição pode ser adiada por muitos anos quando a modularidade interna é cuidada.

Caminhos de Evolução

Quando um gatilho real aparece, a extração segue o padrão strangler fig: um roteador na frente direciona certas rotas para o novo serviço, que cresce por dentro enquanto o monolito encolhe. Extraia primeiro o módulo com perfil de carga distinto ou com ritmo de mudança próprio, migre o dado gradualmente com sincronização temporária via CDC, e mantenha uma camada anticorrupção enquanto os dois mundos convivem. A extração guiada por gatilho observado, e não por moda, mantém o custo proporcional ao benefício.

Gatilho observado Caminho de evolução Cuidado principal
Módulo com perfil de carga muito distinto Extrair como serviço e escalar isolado Migrar o dado com sincronização temporária
Times independentes pisando uns nos outros Monolito modular e, depois, partição por domínio Não extrair serviço antes de fronteiras estáveis
Exigência regulatória de isolamento de dados Serviço separado com banco dedicado Contratos claros e trilha de compliance
Deploy do todo atrasando features locais Pipeline por módulo antes de separar serviço Evitar trocar monolito por monolito distribuído
Ciclo de release de módulos muito diferente Serviço com ciclo de vida próprio Assumir custo de contrato e versionamento
Carga de leitura estourando o primário Réplicas, cache e separação de consultas Verificar consistência exigida por cada leitura
Módulo pede tecnologia de runtime distinta Serviço com runtime próprio e contrato formal Observabilidade e deploy desde o primeiro dia

Erros Comuns e Boas Práticas

Sintoma em produção Causa provável Mitigação
Feature simples atravessa dez módulos Fronteiras não impostas Testes de dependência no pipeline
Regras escondidas em triggers e procedures Lógica fora do versionamento do código Migrar para o módulo dono e versionar schema
Deploy precisa de madrugada e checklist Acoplamento interno e lotes grandes Modularizar e reduzir lote com automação
Módulo consultando tabela alheia direto Sem dono de dado declarado Schema por módulo e acesso só via API
Testes lentos demais para rodar local Acoplamento e ambiente gigante Testes de unidade por módulo e integração focada

Boas práticas consolidadas para o dia a dia:

  • Declare dono por módulo: cada módulo tem um responsável nominal, e mudanças cruzadas precisam de aprovação do dono.
  • Imponha fronteiras por ferramenta: regra que depende de disciplina vira sugestão no primeiro prazo apertado.
  • Trate o schema como fronteira de dado: tabela pertence a um módulo, e integração é por interface ou evento interno.
  • Monitore o build e a suíte de testes: tempo de build é medidor precoce de acoplamento; crescimento constante pede redesenho.
  • Mantenha um plano de extração escrito: quais módulos virariam serviço, em que ordem e com qual gatilho, para que a evolução seja decisão e não surpresa.
  • Meça a modularidade periodicamente: métricas de dependência entre pacotes e de tamanho por módulo revelam deriva antes do build explodir.
  • Recuse o atalho só dessa vez: importação fora da porta pública, aprovada sob pressão, reinstala a bola de lama em poucos sprints.