Separando a regra de negócio dos controllers (com Interactors e Organizers)
Neste post você verá como refatorar controllers gordos (fat controllers) em aplicações Rails, extraindo a regra de negócio para classes especializadas com Interactors e Organizers.O objetivo é mostrar, com um exemplo real, como essa separação traz um código mais limpo, testável e fácil de manter. MVC Grande parte das aplicações…
Neste post você verá como refatorar controllers gordos (fat controllers) em aplicações Rails, extraindo a regra de negócio para classes especializadas com Interactors e Organizers.
O objetivo é mostrar, com um exemplo real, como essa separação traz um código mais limpo, testável e fácil de manter.
MVC
Grande parte das aplicações web segue o padrão arquitetural MVC — Model, View, Controller.
O padrão se propõe a separar e delegar o fluxo sobre essas três camadas:
- Model – concentra a lógica de domínio, regras de negócio e persistência de dados.
- View – cuida da apresentação da informação para o usuário (HTML, JSON, etc.).
- Controller – é a ponte entre a requisição externa e o núcleo da aplicação.

Controller
Recebe uma chamada HTTP, interpreta seus parâmetros e coordena o que deve acontecer no domínio da aplicação, retornando uma resposta adequada.
Do contrário, não é responsabilidade do controller:
- Implementar regras de negócio complexas de domínio.
- Fazer integrações externas pesadas (ex.: chamadas a APIs, processamento de arquivos) diretamente.
- Manipular dados em grande escala ou manter lógica de persistência elaborada.
Quando um controller começa a acumular responsabilidades como essas, surge o famoso “fat controller”.
# app/controllers/newsletters_controller.rb
class NewslettersController < ApplicationController
def signup
email = params[:email]
if email.present? && valid_email?(email)
existing_newsletter = Newsletter.find_by(email: email)
if existing_newsletter
if existing_newsletter.unsubscribed_at.present?
existing_newsletter.update!(
unsubscribed_at: nil,
resubscribed_at: Time.current
)
message = 'Assinatura reativada com sucesso!'
else
# Email já ativo
return redirect_to root_path, alert: 'Este email já está cadastrado!'
end
else
existing_newsletter = Newsletter.create!(
email: email,
subscribed_at: Time.current
)
message = 'Cadastro realizado com sucesso!'
end
if existing_newsletter.resubscribed_at
NewsletterMailer.reactivation_email(existing_newsletter).deliver_now
else
NewsletterMailer.welcome_email(existing_newsletter).deliver_now
end
redirect_to root_path, notice: message
else
redirect_to root_path, alert: 'Email inválido!'
end
end
private
def valid_email?(email)
email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
end
O que tem de errado aqui?
O controller “sabe demais” sobre o fluxo de negócio.
O chefe de restaurante “faz tudo”
Imagine um restaurante em que um único chefe:
- Recebe o cliente na porta
- Verifica se tem vaga
- Volta para informar
- Leva eles para a mesa
- Anota o pedido
- Prepara todos os pratos
- Cuida do caixa
- Limpa as mesas
- Vai ao estoque repor ingredientes
Como deveria ser:
- Recepcionista → recebe clientes e anota pedidos.
- Chefs → cozinham cada tipo de prato.
- Caixa → cuida do pagamento.
- Equipe de limpeza → mantém o ambiente pronto.
Interactor
Um Interactor é uma classe que encapsula um caso de uso específico da aplicação, concentrando toda a regra de negócio necessária para executar essa ação.
Ele segue o princípio de Single Responsibility: cada interactor faz apenas uma coisa, de ponta a ponta, e expõe um método principal (call
) para ser usado pelo controller.Como funciona?
- Você define uma classe que inclui Interactor.
- O método call recebe o contexto (context) com os dados de entrada.
- Se algo der errado, você pode encerrar o fluxo com context.fail!.
- No final, você grava no context as saídas que o controller precisa.
Context
Interactors funcionam com context. Pode ser entendido como um cofre, onde os dados são armazenados e podem ser acessados.
No nosso Gemfile:
gem "interactor", "~> 3.0"
Estrutura de pastas:
▾ app
▸ controllers
▸ helpers
▾ interactors
newsletter
signup.rb
▸ mailers
▸ models
▸ views
Interactor:
# app/interactors/newsletter/signup.rb
class Newsletters::Signup
include Interactor
def call
email = context.email
unless email.present? && valid_email?(email)
context.fail!(message: 'Email inválido!')
end
newsletter = Newsletter.find_by(email: email)
if newsletter
if newsletter.unsubscribed_at.present?
newsletter.update!(
unsubscribed_at: nil,
resubscribed_at: Time.current
)
NewsletterMailer.reactivation_email(newsletter).deliver_later
context.message = 'Assinatura reativada com sucesso!'
else
context.fail!(message: 'Este email já está cadastrado!')
end
else
newsletter = Newsletter.create!(
email: email,
subscribed_at: Time.current
)
NewsletterMailer.welcome_email(newsletter).deliver_later
context.message = 'Cadastro realizado com sucesso!'
end
context.newsletter = newsletter
end
private
def valid_email?(email)
email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
end
Controller:
class NewslettersController < ApplicationController
def signup
result = Newsletter::Signup.call(email: params[:email])
if result.success?
redirect_to root_path, notice: result.message
else
redirect_to root_path, alert: result.message
end
end
end
Melhorias:
- O controller volta a ser apenas a ponte entre requisição e resposta.
- Toda a política de negócio vive no Interactor.

Organizer
À medida que o sistema cresce, um único interactor pode voltar a ficar “gordo”.
Quando a lógica de negócio se desdobra em vários passos independentes, a gem interactor
oferece um recurso perfeito: o Organizer.
Compartilhando o context
O Organizer é um interactor que orquestra outros interactors.
Em vez de escrever toda a lógica em um único call
, você cria pequenos interactors especializados (ex.: validar dados, criar ou atualizar registro, enviar e-mail) e o Organizer executa cada um em sequência, compartilhando o contexto entre eles.
Esse padrão ajuda a manter alta coesão e baixo acoplamento, além de deixar o fluxo de negócio explícito.
Quando usar
- O caso de uso já tem múltiplas responsabilidades distintas.
- Existe possibilidade de crescimento: novos passos, integrações externas, métricas, double opt-in etc.
- Há reuso de passos em outros fluxos.
- Sequência determinadas de passos dependentes
Exemplo:
Refatorando o interactor Newsletters::Signup
em vários passos:
class Newsletters::ValidateEmail
include Interactor
def call
email = context.params[:email].to_s.strip
unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
context.fail!(message: 'Email inválido!')
end
context.email = email
end
end
class Newsletters::UpsertSubscription
include Interactor
def call
newsletter = Newsletter.find_by(email: context.email)
if newsletter
if newsletter.unsubscribed_at.present?
newsletter.update!(unsubscribed_at: nil, resubscribed_at: Time.current)
context.action = :reactivated
else
context.fail!(message: 'Este email já está cadastrado!')
end
else
newsletter = Newsletter.create!(email: context.email, subscribed_at: Time.current)
context.action = :created
end
context.newsletter = newsletter
end
end
class Newsletters::SendEmail include Interactor def call case context.action when :reactivated NewsletterMailer.reactivation_email(context.newsletter).deliver_later context.message = 'Assinatura reativada com sucesso!' when :created NewsletterMailer.welcome_email(context.newsletter).deliver_later context.message = 'Cadastro realizado com sucesso!' end end end
O Organizer orquestra esses passos:
class Newsletters::SignupOrganizer
include Interactor::Organizer
organize Newsletters::ValidateEmail,
Newsletters::UpsertSubscription,
Newsletters::SendEmail
end
E o controller permanece enxuto:
class NewslettersController < ApplicationController
def signup
result = Newsletters::SignupOrganizer.call(params: params)
if result.success?
redirect_to root_path, notice: result.message
else
redirect_to root_path, alert: result.message
end
end
end
Benefícios
- Código modular: cada interactor é pequeno e de responsabilidade única.
- Alta testabilidade: é possível testar cada passo isoladamente.
- Facilidade de evolução: adicionar uma etapa (ex.: analytics, CRM) significa apenas criar um novo interactor e adicioná-lo ao
organize
. - Clareza: o fluxo de negócio é visível em uma linha no Organizer.