Um monolito modular mantém uma unidade de implantação e organiza seu código em módulos com responsabilidades, interfaces e dados claramente delimitados. O trabalho de arquitetura consiste em decidir quais mudanças cada módulo controla, por quais contratos os demais podem solicitar essas mudanças e como verificar que essas fronteiras continuam válidas. Essa organização permite evoluir partes da aplicação com contexto limitado, enquanto o processo de entrega permanece compartilhado.
O ganho depende da qualidade dessas decisões. Um módulo chamado “clientes” que expõe todas as suas tabelas, entidades e regras permite que qualquer funcionalidade passe a depender de seus detalhes. Quando uma mudança chega, a equipe precisa localizar consumidores dispersos antes de saber o que pode alterar. A separação de diretórios existe, mas a autonomia de manutenção continua pequena.
Considere, ao longo deste artigo, um sistema fictício de inscrições em cursos. O exemplo foi construído para discutir arquitetura; não representa uma implantação medida. A aplicação reúne catálogo de turmas, inscrições, pagamentos e comunicação. A equipe quer acrescentar lista de espera, diferentes formas de pagamento e notificações sem transformar cada alteração em uma revisão de todo o produto. A decisão inicial será manter uma aplicação implantável e definir melhor seus limites internos.
Comece pelas regras que precisam permanecer verdadeiras
Uma fronteira útil agrupa decisões que precisam ser compreendidas e alteradas em conjunto. No sistema de cursos, a capacidade de uma turma e a ocupação de suas vagas participam da mesma regra: inscrições confirmadas mais reservas ainda válidas não devem ultrapassar a capacidade autorizada. Essa condição é uma invariante, isto é, uma propriedade que o sistema precisa preservar mesmo diante de solicitações concorrentes e falhas.
No exemplo, confirmar uma inscrição converte uma reserva válida em ocupação confirmada, sem contar a mesma vaga duas vezes. Expirar ou liberar uma reserva retira seu consumo de capacidade. Uma redução da capacidade autorizada também precisa preservar a condição ou acionar um tratamento de exceção de negócio explícito, em vez de deixar o sistema com uma promessa de vaga que não consegue cumprir.
Escrever a invariante ajuda a perceber um problema no desenho preliminar. Se catálogo altera capacidade e inscrições confirma vagas sem coordenação, cada módulo pode executar uma operação localmente válida e produzir um estado global inválido. Antes de escolher comunicação assíncrona ou criar interfaces, a equipe precisa definir quem tem autoridade sobre essa regra. No exemplo, um módulo de oferta e ocupação passa a controlar capacidade, reserva e confirmação de vagas.
Essa escolha não obriga todo dado sobre uma turma a morar no mesmo lugar. A descrição comercial, a imagem e os materiais podem continuar no catálogo. O ponto é separar a informação exibida da autoridade que aceita uma reserva. Uma página pode mostrar uma disponibilidade aproximada; a confirmação exige a verificação na operação que controla a ocupação. O contrato deve deixar essa diferença visível.
Use situações reais de mudança para testar uma fronteira proposta. Pergunte onde seria implementado um novo prazo de reserva, quem decide o destino de uma inscrição sem pagamento e quais componentes precisam mudar quando a regra de cancelamento é alterada. Se a resposta atravessa todos os módulos sem uma razão de negócio, o desenho está distribuindo uma mesma responsabilidade por lugares demais.
Também existem dependências legítimas. Pagamentos precisa conhecer o identificador e o valor de uma cobrança; comunicação precisa receber os dados necessários para avisar o participante. O objetivo é tornar essas relações pequenas e explícitas. Separar os módulos implica aceitar contratos e coordenação nos pontos em que uma capacidade depende de outra, com o custo correspondente de evolução e teste.
Descreva o que cada módulo oferece e protege
Para cada módulo, registre uma ficha curta: responsabilidade, regras protegidas, operações públicas, dados sob sua autoridade e dependências permitidas. Esse registro funciona como referência durante revisão de código e planejamento. Uma equipe que precisa de uma nova operação consegue avaliar se ela cabe no contrato existente ou se há uma mudança de responsabilidade a discutir.
No exemplo, inscrições oferece uma operação para iniciar a inscrição e outra para consultar seu estado. A entrada inclui identidade do participante, turma e identificador da solicitação. O retorno informa o resultado reconhecido pelo domínio, como aguardando pagamento ou indisponível. Detalhes de tabela, mecanismo de persistência e entidades mutáveis permanecem internos. O consumidor recebe a informação necessária para continuar sua tarefa.
Evite contratos genéricos que permitem qualquer alteração, como atualizar uma entidade com um conjunto arbitrário de campos. Uma operação com intenção explícita, como cancelar uma reserva, permite verificar motivo, prazo, estado anterior e autorização. Essa escolha também torna a mudança legível nos registros operacionais. Um campo de status alterado isoladamente não informa quais regras foram avaliadas.
A verificação estrutural do Spring Modulith 1.4 ilustra uma forma concreta de fiscalizar esse arranjo: detectar ciclos entre módulos, rejeitar acesso a pacotes internos de módulos fechados e, opcionalmente, restringir dependências a uma lista autorizada. Configurar um módulo como aberto altera a restrição de acesso aos seus internos. São verificações sobre o modelo configurado. Elas não demonstram, sozinhas, que a divisão corresponde às regras do negócio.
| Módulo do exemplo | Autoridade principal | Contrato oferecido | Detalhe que preserva internamente |
|---|---|---|---|
| Oferta e ocupação | Capacidade e vagas reservadas | Reservar, confirmar e liberar vaga | Estrutura usada para controlar concorrência |
| Inscrições | Jornada e estado da inscrição | Iniciar, cancelar e consultar inscrição | Transições e histórico interno |
| Pagamentos | Situação da cobrança | Solicitar cobrança e consultar resultado | Adaptação ao provedor financeiro |
| Comunicação | Entrega de avisos | Receber solicitação de comunicação | Templates, tentativas e fornecedor |

Uma interface pública também precisa de estabilidade semântica. Manter o mesmo nome de método enquanto altera o significado de “confirmado” quebra o consumidor de forma menos visível que um erro de compilação. Documente estados e condições de sucesso, incluindo o que ainda pode acontecer depois do retorno. Esse cuidado torna a compatibilidade verificável durante mudanças que atravessam mais de um módulo.
Faça os dados obedecerem à mesma fronteira
Uma aplicação única pode utilizar um único banco e ainda atribuir a cada módulo a autoridade sobre determinadas tabelas. No desenho proposto, pagamentos não atualiza diretamente a ocupação de uma turma. Ele comunica um resultado por um contrato definido, e o componente responsável decide a transição permitida. Essa disciplina impede que uma alteração incidental de armazenamento se transforme em dependência permanente de outro domínio.
A propriedade lógica precisa aparecer em revisões, convenções de acesso e testes. Separar schemas auxilia a identificação, mas não cria isolamento automaticamente quando todos os componentes usam uma credencial com acesso amplo. Da mesma forma, uma interface pode ser contornada por uma consulta SQL escrita em outro lugar. O controle tem de alcançar os caminhos efetivamente usados pela aplicação.
Consultas de leitura merecem uma decisão própria. Um relatório administrativo pode precisar combinar inscrição, turma e pagamento. Obrigar toda consulta a atravessar muitas interfaces pequenas pode produzir complexidade e custo desnecessários. Uma alternativa é manter uma projeção de leitura com contrato explícito, atualizada a partir dos dados autorizados. Outra é permitir consultas específicas por uma interface de leitura mantida pelo módulo proprietário.
Quando a equipe autorizar uma consulta direta para relatórios, registre quais tabelas ou visões fazem parte desse contrato e quem verifica a compatibilidade. Essa exceção amplia a superfície de mudança: renomear um campo ou alterar seu significado passa a exigir coordenação com o relatório. A decisão pode ser adequada, desde que seu custo esteja visível e não autorize gravações por conveniência.
Considere ainda os dados pessoais compartilhados. Copiar todos os atributos do participante para cada módulo aumenta os pontos que precisam de proteção e atualização. O exemplo utiliza identificadores estáveis e transfere apenas os campos necessários para a tarefa. A definição concreta depende das obrigações do produto e de seus controles; a arquitetura deve permitir localizar onde um dado é usado e corrigir cópias quando necessário.
Escolha a transação a partir da invariante
O processo compartilhado facilita operações locais, mas a simples proximidade do código não garante consistência. Duas requisições podem disputar a última vaga ao mesmo tempo. Se ambas leem disponibilidade e depois confirmam sem coordenação adequada, a aplicação pode aceitar duas inscrições. A solução precisa combinar regra de domínio, mecanismo de concorrência e restrições de persistência compatíveis com o caso.
No cenário proposto, a reserva é aceita por uma operação atômica do módulo de oferta e ocupação. Ela verifica e altera a disponibilidade sob um mecanismo de concorrência escolhido pela equipe, preservando a invariante. O artigo não apresenta uma implementação executada: bloqueio, atualização condicional ou outra estratégia precisam ser validados no banco e na carga utilizados, inclusive nas condições de disputa.
A documentação de isolamento do PostgreSQL 18 mostra por que “usar transação” é uma descrição insuficiente. Em Read Committed, duas consultas sucessivas podem observar estados diferentes. Serializable oferece garantias mais fortes sobre transações confirmadas, mas a aplicação precisa tratar falhas de serialização e repetir a transação completa quando apropriado. O nível escolhido altera o que o código pode assumir.
Decida também onde a transação começa e termina. Se iniciar inscrição e reservar vaga precisam confirmar juntos no mesmo banco, um serviço de aplicação pode coordenar as operações públicas participantes em uma transação local. Neste exemplo, o coordenador pertence a inscrições: conduz a jornada e solicita ao módulo de oferta as operações que protegem a disponibilidade. As interfaces devem deixar clara sua participação transacional. Um módulo não deve abrir outra transação independente sem considerar o compromisso de atomicidade assumido pela operação superior.
O pagamento externo introduz outra condição. Uma transação local não desfaz automaticamente uma cobrança confirmada em um provedor remoto. Manter locks enquanto espera uma chamada de rede ainda amplia o período de contenção. No exemplo, a inscrição assume estados intermediários: vaga reservada, pagamento pendente e confirmação concluída. Prazo da reserva e tratamento de resultados tardios fazem parte da regra que produto e engenharia precisam definir.
Essa decisão muda o comportamento percebido pelo participante. Se o pagamento chega depois de a vaga ter sido liberada, o sistema precisa de um procedimento explícito: reconciliar, tentar outra alocação permitida ou iniciar o tratamento financeiro previsto. “Falhou” é um estado pouco informativo para uma operação com efeito externo confirmado. Os contratos internos devem representar essas possibilidades de forma que a operação consiga agir.
Use eventos com condições de entrega explícitas
Eventos internos ajudam quando uma ocorrência interessa a várias capacidades. Depois de confirmar uma inscrição, comunicação pode preparar um aviso e uma projeção pode atualizar uma consulta administrativa. O produtor declara o fato com identificador e informações estáveis; consumidores decidem como reagir dentro de suas responsabilidades. Isso reduz a necessidade de acrescentar cada nova reação ao método que confirmou a inscrição.
É necessário escolher como essas reações participam da execução. Um evento síncrono no mesmo processo pode manter o consumidor dentro da transação de origem e propagar sua falha. Um evento tratado depois da confirmação da transação exige outro mecanismo para preservar o trabalho pendente. Esses comportamentos têm consequências diferentes para latência, consistência e recuperação.
A documentação de eventos do Spring Modulith 1.4 explicita essa diferença. O texto descreve o processamento síncrono padrão e o risco de perda quando uma reação assíncrona falha sem proteção. Também apresenta um registro persistente de publicações que participa da transação original. A existência desse recurso não elimina a necessidade de configurar recuperação e lidar com repetição no consumidor.
No sistema fictício, a confirmação da inscrição registra um aviso pendente na mesma transação do estado confirmado. Um executor posterior tenta entregar o aviso, registra o resultado e permite nova tentativa quando a política autoriza. Se a aplicação encerrar antes do envio, o registro pendente permanece disponível. Se encerrar depois do envio e antes de registrar o sucesso, a repetição precisa ser considerada no contrato com o serviço de comunicação.

Esse mecanismo custa armazenamento, observação, política de tentativa e tratamento de itens que não avançam. Para uma operação auxiliar que pode ser abandonada sem consequência relevante, outra política pode ser suficiente. Para um comprovante necessário ao participante, perder o trabalho silenciosamente pode ser inaceitável. A escolha deve explicar a obrigação atendida, e não apenas o padrão técnico utilizado.
Torne as dependências proibidas detectáveis
Uma convenção que depende exclusivamente da memória da equipe fica vulnerável a atalhos durante mudanças urgentes. Traduza os limites mais importantes em verificações automáticas. No caso fictício, apenas oferta e ocupação pode importar sua persistência; comunicação não pode alterar estados de inscrição; infraestrutura compartilhada não deve depender dos módulos de negócio que a utilizam.
Em projetos JavaScript ou TypeScript organizados com Nx, a regra de limites entre módulos permite expressar restrições entre projetos por tags e dependências. A adequação depende de os projetos representarem as fronteiras que a equipe pretende proteger. Uma regra de importação não inspeciona automaticamente toda consulta ao banco ou toda chamada dinâmica; esses caminhos exigem controles complementares.
Uma revisão útil testa a própria regra. Introduza, em uma alteração temporária de laboratório, um acesso proibido e confirme que a integração contínua o rejeita. Depois remova a violação. O resultado verifica um caminho específico de detecção; não autoriza afirmar que qualquer contorno será encontrado. Esse exercício evita confiar em uma configuração que nunca foi confrontada com o problema que deveria detectar.
Registre exceções de forma restrita. Se uma transição exige permitir um acesso específico, identifique origem, destino, motivo, responsável e condição de retirada. Liberar todos os imports de um módulo para resolver uma dependência pontual enfraquece uma regra inteira. A exceção deve ficar visível no lugar em que a dependência é avaliada, com teste ou revisão que confirme sua necessidade ao longo do tempo.
Observe também o crescimento de bibliotecas compartilhadas. Tipos de transporte, relógio e ferramentas técnicas podem ser reutilizados com pouca relação com o domínio. Uma biblioteca que passa a concentrar regras de preço, autorização de inscrição e pagamento se torna um ponto de mudança transversal. A equipe precisa perguntar quem responde por essas decisões e se a reutilização está escondendo uma responsabilidade que deveria estar nomeada.
Teste o comportamento dos módulos e suas ligações
O primeiro conjunto de testes protege regras locais. Para oferta e ocupação, isso inclui liberar uma reserva, recusar uma confirmação inválida e manter a capacidade sob solicitações concorrentes. Para inscrições, inclui transições permitidas e resultados tardios. Os testes devem partir das regras esperadas, com exemplos capazes de distinguir o resultado correto de uma implementação apenas plausível.
O segundo conjunto verifica contratos entre módulos. No exemplo, um consumidor recebe um pagamento confirmado com identificador desconhecido; outro recebe o mesmo resultado mais de uma vez. A equipe precisa observar a resposta prevista, o estado persistido e a possibilidade de acompanhamento operacional. Substitutos de dependência ajudam testes locais, mas sua fidelidade deve ser comparada ao comportamento do componente real em testes de integração.
Inclua cenários que atravessam a transação e o processo. Uma falha antes da confirmação local deve produzir um resultado diferente de uma falha após a gravação do aviso pendente. Encerrar e reiniciar o executor revela se o trabalho é recuperável. Esses testes exigem ambiente apropriado e dados controlados; listar os cenários no plano não equivale a executá-los ou comprovar suas garantias.
Mantenha algumas jornadas completas para verificar a montagem da aplicação. Elas podem encontrar problemas de configuração, mapeamento e integração que testes isolados não alcançam. A quantidade depende dos riscos, do tempo de execução e da estabilidade. Concentrar toda verificação nas jornadas completas dificulta localizar a origem de uma falha; concentrá-la apenas em funções locais deixa contratos relevantes sem observação.

Coordene alterações de contrato e armazenamento
Uma aplicação com implantação única ainda pode executar versões diferentes por alguns instantes durante uma atualização gradual. Processos de trabalho em segundo plano também podem concluir tarefas iniciadas com um formato anterior. A equipe deve conhecer seu mecanismo de implantação antes de assumir que todos os consumidores mudarão simultaneamente. O contrato entre módulos precisa considerar essa janela quando alcança dados persistidos ou trabalho enfileirado.
Considere uma alteração didática na reserva de vagas: o produto passa a exigir um motivo de cancelamento. Primeiro, defina o tratamento dos registros antigos e de chamadas que ainda não enviam o motivo. Depois, adicione o armazenamento e o comportamento compatível com essa condição. Só torne o campo obrigatório quando consumidores, dados existentes e operações pendentes estiverem adequados. A sequência concreta depende da infraestrutura e da política do produto.
Esse planejamento evita uma confusão frequente entre mudança de código e mudança de informação. Reverter a versão da aplicação não desfaz automaticamente uma transformação de dados nem recupera um campo removido. Descreva o que será possível fazer se a nova versão falhar após gravar informações no novo formato. Quando a reversão não preservar o significado dos registros, prepare uma correção progressiva e um procedimento operacional verificável.
O proprietário do módulo deve revisar as migrações de seu armazenamento e avisar os consumidores dos contratos afetados. No exemplo, a alteração de uma visão de relatório precisa ser coordenada com quem a utiliza, mesmo que a visão seja somente leitura. A autonomia de manutenção existe dentro dos compromissos publicados. Registrar dependências autorizadas permite descobrir esses compromissos antes do deploy, com tempo para testar os dois lados da mudança.
Acompanhe o custo real das fronteiras
A modularidade pode ser avaliada durante mudanças concretas. Registre quais módulos uma alteração precisou tocar e por quê. Uma mudança que atravessa inscrições e pagamentos é esperada quando o produto muda a política de cobrança. A mesma mudança exigindo ajustes em comunicação, relatórios e utilitários por causa de um detalhe de persistência indica outro tipo de dependência, que merece investigação.
Evite transformar o número de módulos alterados em meta isolada. A equipe conseguiria reduzir esse número agrupando tudo em um único módulo, sem melhorar a capacidade de manutenção. O indicador serve para encontrar casos e perguntas: a interface mudou? Um conceito está representado duas vezes? O consumidor depende de uma estrutura interna? O domínio exige coordenação que o desenho não reconheceu?
Acompanhe ainda tempo de revisão, conflitos frequentes nas mesmas áreas e dificuldade para testar um módulo sem montar toda a aplicação. Esses sinais orientam uma investigação, mas não provam que a arquitetura é a causa exclusiva. Rotatividade, critérios de aceite imprecisos e mudanças simultâneas também afetam o trabalho. Examine exemplos de alterações antes de atribuir todo atraso ao estilo arquitetural.
Uma unidade de implantação continua compartilhando recursos e consequências. Uma consulta pesada pode competir por conexões; uma falha de processo pode afetar mais de uma capacidade; uma atualização exige compatibilidade do conjunto implantado. Limites internos ajudam a localizar decisões e dependências, mas não criam automaticamente isolamento de falha, orçamento de recursos ou autonomia de deploy. Essas propriedades precisam de mecanismos específicos e de verificação própria.
Reavalie a implantação quando existir uma razão concreta
Separar um módulo em serviço faz sentido quando há um problema suficientemente importante de escala, isolamento, autonomia de entrega ou responsabilidade operacional. Defina o problema com evidência: fila de deploy que bloqueia uma equipe, recurso que cresce de maneira distinta ou falha cuja contenção exige outro processo. Compare a separação com alternativas menores, como reduzir contenção, reorganizar execução ou corrigir uma consulta.
A extração acrescenta comunicação remota, autenticação entre componentes, observabilidade distribuída e novos estados de falha. Uma chamada que antes retornava ou lançava uma exceção pode terminar com resultado desconhecido depois de um timeout. Uma transação local antes compartilhada deixa de oferecer a mesma atomicidade. Se o módulo depende continuamente de dados e transações dos demais, sua interface ainda não descreve uma autonomia operacional suficiente.
O roteiro de evolução começa pelo contrato que já existe: identifique consumidores, elimine acessos internos não autorizados, explicite estados intermediários e exercite falhas. Depois estime o custo de operar a unidade separada e a responsabilidade de quem a mantém. Esse trabalho produz evidência para a decisão; não exige que todo monolito modular termine convertido em serviços.
No sistema de cursos, comunicação é candidata a isolamento quando sua carga ou seu fornecedor ameaça a capacidade necessária para inscrições. A equipe deve medir essa interferência antes de assumir que a separação resolverá o problema. Enquanto os limites internos, as transações e as verificações sustentarem as mudanças previstas, manter a implantação conjunta é uma decisão arquitetural válida e acompanhável.
Fontes
- Spring Modulith 1.4 — Verifying Application Module Structure. Verificação de ciclos, acesso a pacotes internos e dependências permitidas.
- PostgreSQL 18 — Transaction Isolation. Garantias dos níveis de isolamento e tratamento de falhas de serialização.
- Spring Modulith 1.4 — Working with Application Events. Participação transacional de eventos e registro persistente de publicações.
- Nx — Enforce Module Boundaries Rule. Restrições entre projetos JavaScript/TypeScript por tags e dependências.




