01 - Monolito (Monolithic)
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.