Home Projetos & Releases BigBangcraft Blog Sobre & Carreira
Ruby on Rails • Hub 7 min de leitura 25 visualizações
Raw .md

O monólito modular no Rails 8 e a fronteira que quase todo time erra

PD

Pedro Dalben

CTO no Integrar Plus • Publicado em 29 de September de 2026

O monólito modular no Rails 8 e a fronteira que quase todo time erra

A discussão sobre arquitetura no ecossistema Rails costuma oscilar entre dois extremos igualmente cansativos. De um lado, defensores ferrenhos de microserviços tentam convencer qualquer equipe de cinco pessoas de que separar faturamento e autenticação em repositórios distintos vai resolver problemas de organização de código. Do outro, tradicionalistas do monólito afirmam que basta criar subpastas dentro de app/models, confiar na disciplina coletiva e deixar o ActiveRecord conectar tudo livremente com dezenas de has_many encadeados.

Ambas as visões falham feio na prática.

Microserviços cobram um pedágio operacional desproporcional para quem roda infraestrutura enxuta em servidores dedicados ou VPS. Você troca chamadas de método em memória por latência de rede, falhas parciais, filas extras e complexidade de deploys com Kamal. Por outro lado, o monólito caótico, onde a classe de envio de e-mails consulta diretamente tabelas de cobrança e altera registros de usuários via callbacks ocultos, cobra o preço na velocidade de manutenção e no medo constante de rodar migrações.

Eu fico no meio do caminho, mas discordo do consenso recente que tenta vender engines do Rails ou ferramentas de isolamento estático como soluções mágicas. O Rails 8 entrega uma base excelente para manter tudo no mesmo processo sem precisar fatiar a aplicação em dez serviços, desde que a separação respeite o lugar onde os domínios realmente se misturam: o banco de dados.

O problema nunca foi a estrutura de pastas

Quem tenta organizar um monólito modular quase sempre começa movendo arquivos. Cria-se uma pasta para cada domínio, adota-se ferramentas de análise de dependências ou monta-se engines completas com suas próprias rotas e controladores. No papel, o diagrama fica limpo.

Na execução real, esse isolamento costuma ser cosmético. Se o modelo Subscription dentro do módulo de cobrança continua fazendo belongs_to :account, e um desenvolvedor chama subscription.account.update!(status: :active) dentro de um controller de faturamento, a barreira arquitetural ruiu por completo. O arquivo estava em outra pasta, mas a fronteira foi ignorada pelo ActiveRecord no primeiro acesso relacional.

Engines do Rails prometiam resolver isso, mas cobram um preço alto de atrito. Elas complicam o carregamento de código no Zeitwerk, exigem configurações repetidas de rotas e testes, e adicionam uma camada burocrática que a maioria das equipes abandona após seis meses.

O isolamento real em Ruby precisa de convenções operacionais simples, não de barreiras artificiais de empacotamento. Três regras pragmáticas sustentam um monólito limpo sem afastar o projeto do jeito Rails de ser:

  1. Comunicação entre domínios acontece apenas por objetos de entrada explícitos, nunca por consultas diretas aos modelos internos de outro domínio.
  2. Modelos de um domínio não declaram associações diretas do ActiveRecord com tabelas de outros domínios; guardam apenas o identificador numérico.
  3. Transações de banco de dados não podem englobar regras de negócio de múltiplos domínios.

Quebrando a teia de associações do ActiveRecord

O ActiveRecord é a ferramenta mais produtiva do Rails e, ao mesmo tempo, a principal armadilha contra limites de contexto. A tentação de encadear chamadas até atravessar cinco tabelas diferentes é constante porque o framework torna isso quase invisível.

Quando construo módulos no Rails 8, a primeira decisão é proibir associações diretas entre domínios distintos. Se o módulo Billing precisa saber dados da empresa que está sendo cobrada, ele não faz has_one :organization.

## app/domains/billing/models/billing/invoice.rb
module Billing
  class Invoice < ApplicationRecord
    # Errado: belongs_to :organization, class_name: "Identity::Organization"
    # Correto: guarda apenas a chave estrangeira sem associacao do ORM
    validates :organization_id, presence: true

    def organization
      @organization ||= Identity::FetchOrganization.call(organization_id)
    end
  end
end

Pode parecer heresia abrir mão de um belongs_to nativo em um framework que vive de convenções relacionais. Contudo, no momento em que você remove a associação direta do ORM, corta a raiz de carregamentos preguiçosos indesejados e consultas acidentais em lote que derrubam o PostgreSQL em produção.

O domínio Identity passa a ter um ponto de contato explícito:

## app/domains/identity/services/identity/fetch_organization.rb
module Identity
  class FetchOrganization
    OrganizationData = Data.define(:id, :name, :tax_id, :active)

    def self.call(organization_id)
      org = Organization.find(organization_id)
      OrganizationData.new(
        id: org.id,
        name: org.name,
        tax_id: org.tax_id,
        active: org.active?
      )
    end
  end
end

Retornar um objeto de dados imutável em vez da instância de ActiveRecord::Base impede que o módulo chamador tente invocar métodos modificadores ou salvar alterações indiretas no banco de dados.

A ilusão da transação compartilhada

O ponto de quebra mais perigoso em monólitos modulares é a transação de banco de dados distribuída implicitamente. Dois domínios diferentes operam no mesmo banco, compartilham a mesma conexão e, frequentemente, rodam dentro do mesmo bloco ActiveRecord::Base.transaction.

Considere um fluxo comum: o usuário confirma um pagamento, o domínio de faturamento registra a fatura e, na mesma transação, dispara uma chamada para o domínio de notificações enviar uma mensagem e atualizar o log de auditoria.

Se a escrita na tabela de auditoria falhar por conta de uma coluna nula, o pagamento inteiro sofre rollback no banco. Pior ainda: se o domínio de notificações depender de um lock na tabela de usuários, uma operação simples de faturamento passa a concorrer por conexões com a tela de login.

A regra precisa ser rígida: um domínio nunca abre transação cobrindo a execução de outro. A consistência entre fronteiras de domínio deve ser eventual.

No Rails 8, isso se tornou consideravelmente mais simples de operar graças à maturidade do Solid Queue rodando diretamente sobre o PostgreSQL. O domínio de origem executa sua operação, persiste o estado em sua tabela e enfileira um job transacional para que o domínio consumidor processe a reação de forma isolada.

module Billing
  class ConfirmPayment
    def self.call(invoice_id:)
      Invoice.transaction do
        invoice = Invoice.lock.find(invoice_id)
        invoice.update!(status: :paid, paid_at: Time.current)

        # O job grava na tabela do Solid Queue dentro da mesma transacao
        Billing::PaymentConfirmedJob.perform_later(invoice.id)
      end
    end
  end
end

O Solid Queue usa o próprio banco relacional como backend. Como a gravação do job faz parte da transação do Invoice, não existe risco de enfileirar uma mensagem cujo registro falhou ao gravar. O domínio de notificações, por sua vez, consome o evento em seu próprio ciclo, com sua própria conexão e sem ameaçar a integridade financeira.

Onde a disciplina costuma ceder

Separar código desse jeito exige maturidade do time. A flexibilidade do Ruby faz com que qualquer desenvolvedor com pressa consiga furar o encapsulamento chamando uma constante privada ou executando um ActiveRecord::Base.connection.execute improvisado.

Existem ferramentas que ajudam a auditar fronteiras estáticas no código sem exigir a burocracia de engines, mas nenhuma ferramenta substitui revisões de pull request focadas em conexões de banco e escopo de transações.

Eu prefiro tolerar alguma duplicação de estruturas e ter que escrever consultas pontuais a mais do que gerenciar uma malha de microserviços em produção para atender um volume que um único servidor de doze núcleos roda com folga. O monólito bem desenhado não precisa fingir que é composto por dez sistemas isolados; ele só precisa lembrar que tabelas vizinhas não pertencem ao mesmo dono.

PD

Sobre Pedro Dalben

CTO e arquiteto full stack especializado em Ruby on Rails, Vue.js, sistemas multi-tenant e infraestrutura distribuída.