Auditando um serviço Scala contra as quatro restrições regenerativas de Chad Fowler
Levei um serviço Scala de processamento de pedidos das minhas anotações pelas quatro restrições regenerativas de Chad Fowler. Duas passaram de graça, duas forçariam um redesign de verdade. Aqui está o que aprendi sobre onde "módulo fracamente acoplado" termina e "componente regenerativo" começa, e quais partes do redesign eu de fato pagaria.
Em um post de fragmentos no dia 16 de março de 2026, Martin Fowler destacou o ensaio "Compile to Architecture" de Chad Fowler, que argumenta que quando código é barato de gerar, a restrição muda de escrever software para substituí-lo com segurança. A capacidade de substituição, descobre-se, exige quatro propriedades arquiteturais específicas. Li o texto, abri as anotações que mantenho sobre um serviço Scala de processamento de pedidos que venho estudando, e o passei por cada restrição. O resultado foi desigual: duas das quatro se sustentaram sem esforço; as outras duas forçariam um redesign que não tenho certeza se pagaria de fato.
Este post é o registro dessa auditoria, e o que ela me ensinou sobre onde "módulo fracamente acoplado" termina e "componente regenerativo" começa.
As quatro restrições, em termos claros
O ensaio de Chad Fowler propõe que uma arquitetura ganha o adjetivo "regenerativa" quando impõe quatro regras aos seus componentes.
Padrões de comunicação são poucos e uniformes. Um sistema pode usar RPC, eventos, mensagens de actor, command buses ou um log de mutação. Escolha um conjunto pequeno. Substituir um componente então não exige redescobrir qual transporte ele usa ou que suposições de estado vivem dentro da chamada.
Cada dataset tem exatamente um escritor. A frase de Chad Fowler é "autoridade exclusiva de mutação para cada dataset a um único componente". Se dois componentes mutam a mesma tabela ou stream, nenhum dos dois pode ser substituído com segurança — o acoplamento invisível vive nas escritas, não nas leituras.
Superfícies de avaliação são independentes da implementação. Testes de contrato, invariantes de domínio, regras de consistência de eventos, testes de propriedade na fronteira. O comportamento precisa ser verificável sem subir o sistema inteiro. Sem essa superfície, substituição vira chute.
Componentes têm o tamanho do seu grão natural. Grandes demais, regeneração fica arriscada. Pequenos demais, o overhead de coordenação domina. O grão vem da interseção das duas restrições anteriores — uma unidade que possui mutação para alguns dados e pode ser verificada de forma independente é mais ou menos do tamanho certo.
Essas parecem reformulações de conselhos familiares de modularidade. Não são. A barra é mais afiada. "Acoplamento fraco" tolera dois componentes lendo a mesma tabela; autoridade exclusiva de mutação não. "Alta coesão" tolera um módulo de domínio que cuida dos seus próprios jobs em background; a regra do grão natural pergunta se esses jobs compartilham uma superfície verificável com o caminho síncrono.
O serviço que auditei
O serviço é um backend Scala 2.13 rodando em Akka 2.6 e Postgres, com Kafka como bus assíncrono. Ele aceita pedidos por HTTP, passa cada pedido por precificação e validação, reserva inventário, cobra através de uma API de pagamentos externa e emite um stream de eventos. Os testes ficam em três camadas — testes unitários para lógica de domínio pura, testes de integração para a camada de dados, testes de contrato para as superfícies HTTP e de eventos.
Antes de entrar no que passou e no que falhou, o diagrama abaixo mapeia cada componente para a restrição que ele satisfaz ou viola — costuras verdes se sustentam sob auditoria, costuras vermelhas não.
Não vou percorrer cada módulo. As partes interessantes são onde a auditoria quebrou.
O que passou de graça
A verificação de padrão de comunicação se sustentou. O serviço tem exatamente três modos: requisições HTTP na borda, eventos Kafka entre contextos delimitados e mensagens Akka para coordenação efêmera por pedido. Não há um quarto caminho — nenhum script agendado batendo em endpoints de outros serviços, nenhum sistema de arquivos compartilhado, nenhuma leitura direta de banco entre serviços. Quando rastreei um único pedido da requisição HTTP até a liquidação, cada pulo caiu em uma dessas três categorias. Essa previsibilidade é o que faz a primeira restrição passar: uma implementação de substituição só precisa honrar três contratos, não três mais uma cauda longa de "e também".
Superfícies de avaliação também passaram, com uma ressalva. As fronteiras HTTP e Kafka cada uma têm testes de contrato que rodam em milissegundos sem subir o resto do serviço. A camada de domínio tem testes baseados em propriedades para invariantes como "o total de um pedido é igual à soma das linhas menos os descontos". Posso verificar uma engine de precificação reescrita sem tocar no Postgres. A ressalva: a camada de coordenação Akka não tem superfície independente. O comportamento dela é exercitado através de caminhos ponta-a-ponta e inspecionado com logs. Substituí-la significaria reproduzir tráfego de produção e franzir os olhos para os logs, o que não é regenerativo — é depuração.
O que não passou
Autoridade exclusiva de mutação é onde a auditoria ficou desconfortável. No papel, cada módulo é dono das suas tabelas. Na prática, três lugares escrevem na coluna order_status: o handler HTTP do módulo de pedidos, o módulo de pagamentos depois de um reembolso, e um job noturno de reconciliação que vive no mesmo artefato mas roda a partir de um ponto de entrada diferente. Nenhuma dessas escritas passa por uma função compartilhada. Cada uma tem seu próprio SQL.
Esse é exatamente o modo de falha que Chad Fowler avisa — tanto o módulo de pedidos quanto o módulo de pagamentos funcionam em produção hoje, mas nenhum pode ser substituído sem auditar as escritas do outro e o comportamento do job de reconciliação. A correção não é sutil. Ou toda mutação de status é roteada por uma única API no módulo de pedidos — o que significa mover o caminho de código de reembolso e a lógica de reconciliação para dentro dele — ou status vira um componente menor que todo mundo chama. Ambas as opções tocam muito código.
Grão natural falhou por uma razão relacionada. O módulo de pedidos tem aproximadamente 12.000 linhas de Scala envolvendo precificação, validação, transições de status, chaves de idempotência, o job de reconciliação e um exportador de métricas. Cada uma dessas tem uma taxa de mudança diferente e um raio de explosão diferente. Quando a lógica de reconciliação quebrou no último trimestre, a correção tocou arquivos que não tinham nada a ver com reconciliação, porque os testes do módulo sobem tudo. Avaliação independente é impossível por construção. Dividir o módulo seguindo as linhas de propriedade dos dados — pedidos, status, reconciliação — daria três componentes menores, cada um com seus próprios testes de contrato. Também forçaria um compromisso com contratos entre componentes que hoje são chamadas implícitas de método na mesma JVM.
Quando o redesign vale a pena
A formulação de Chad Fowler é explicitamente sobre regeneração por ferramentas — um futuro onde um coding agent reescreve um componente da noite para o dia. Essa barra é alta o suficiente para que qualquer redesign que a satisfaça também compense para mantenedores humanos, mas o custo-benefício muda. Andando pelo redesign no papel, a economia se divide em dois baldes.
Vale a pena fazer: a correção da mutação exclusiva. Mesmo sem nenhum agente à vista, três escritores em uma coluna é o tipo de dívida que produz incidentes do tipo "consertar em um lugar, quebrar em dois". O Tech Radar da Thoughtworks Vol 34 nomeia a mesma dinâmica como dívida cognitiva de codebase — um loop reforçador onde mudanças pequenas começam a disparar falhas inesperadas. Centralizar a mutação de status atrás de uma única API remove uma classe de bugs e desbloqueia qualquer substituição futura.
Vale questionar: a divisão completa de grão. Quebrar o módulo de pedidos em três componentes regeneráveis custa novos contratos, novos formatos de fio, novos targets de build, e um aumento permanente na complexidade de chamadas entre módulos. Para um sistema que humanos vão continuar mantendo por mais três anos, a vitória mais simples é adicionar funções de aptidão arquitetural — pequenas asserções no estilo ArchUnit que falham o build se uma dependência proibida reaparecer — em vez de fisicamente dividir o artefato. O blip "Architecture drift reduction with LLMs" do Tech Radar descreve o mesmo instinto: aplicar a restrição nas costuras sem pagar o custo total de distribuição.
A taxa de aprovação de dois-em-quatro é, em retrospecto, exatamente o que eu esperaria de um serviço que cresceu sob a orientação "baixo acoplamento, alta coesão". Uniformidade de comunicação e superfícies de avaliação são o que codebases disciplinados já constroem. Autoridade exclusiva de mutação e grão do tamanho certo são as partes que silenciosamente escapam quando um deadline aterrissa.
Conclusões
- Audite cada fronteira contra quatro perguntas, em ordem: quem escreve esse dataset, como esse componente fala com o resto, como o comportamento dele é verificado, e o tamanho dele é consistente com as respostas acima.
- Trate escritas compartilhadas como a dívida de mais alta prioridade — elas bloqueiam tanto a substituição humana quanto por agente.
- Adicione uma função de aptidão arquitetural para cada restrição que tenha sido declarada inegociável. ArchUnit, Spring Modulith, ou um conjunto de regras customizado é suficiente.
- Resista a dividir módulos só para satisfazer a regra do grão a menos que a correção de propriedade dos dados force. Distribuição tem seu próprio custo.
Use essas restrições quando o serviço é grande o suficiente para que o modelo mental tenha parado de ser confiável, quando contribuidores assistidos por IA começaram a aterrissar mudanças mais rápido do que revisores conseguem auditar, ou quando uma reescrita planejada está no calendário. Pule quando o serviço é pequeno, quando uma ou duas pessoas são donas dele, e quando a próxima substituição está a um ano de distância — o monólito modular ainda é uma boa resposta nessa escala.
Para leituras adicionais, o ensaio original de Chad Fowler nomeia as restrições; o post de fragmentos de Martin Fowler as localiza dentro da mudança mais ampla de engenharia supervisora; o Tech Radar da Thoughtworks Vol 34 nomeia a gravidade contra a qual essas restrições estão puxando.
Curtindo? Talvez goste disso aqui.
Nada parecido — quer tentar outro ângulo?
Posts Relacionados
Execução Durável Não É Sobre Agentes — É Sobre Workflows de Backend com Replay
Cheguei aos runtimes de execução durável pela hype dos agentes, mas a restrição que surpreende todo mundo é o determinismo no replay. Estas são minhas anotações trabalhando uma reconciliação de pagamentos de seis passos como um workflow do Restate em TypeScript — a linha que quebrou o replay, o modelo mental que consertou, e os trade-offs que vêm com o padrão.
Idempotência É um Protocolo, Não uma Chave
Na primeira vez em que entreguei idempotência como um header UUID e uma consulta no Redis, uma cobrança duplicada escapou uma semana depois. Estas são minhas notas sobre tratar idempotência como um protocolo de quatro partes — deduplicação, determinismo, segurança concorrente, propagação downstream — com uma implementação mínima em Kotlin mais Postgres que se mantém firme sob retry.
Log de Eventos como Fonte da Verdade Transforma Evolução de Schema em um Problema Eterno
Quando o log é a fonte da verdade, toda mudança de schema é permanente. Um passo a passo em Kotlin/Avro do rename que passou na verificação do Schema Registry e corrompeu silenciosamente todos os eventos antigos, mais os invariantes de Protobuf e Avro que agora mantenho fixados acima da minha mesa.